From d0177f0d29f8fb0931634b3e4fc7cc9192c87d7c Mon Sep 17 00:00:00 2001
From: Nev <54870357+MSNev@users.noreply.github.com>
Date: Mon, 23 Feb 2026 17:10:05 -0800
Subject: [PATCH] [Main] Merge Trace API Features from Beta
This commit merges all beta branch features into main with clean linear history. Key changes from beta:
- Merge AppInsightsCommon into AppInsightsCore (Phase 1-4)
- Add W3C Trace State support/handling
- Add startSpan, withSpan, Route Strategy features
- Add tree-shaking annotations
- Update build dependencies and test layouts
- Sync with OTel-SDK file structure
Squash merge of origin/beta (3.4.0-beta) with conflict resolution:
- Updated Rush version to 5.169.3 (from main)
- Removed beta-specific publishConfig
---
.aiAutoMinify.json | 46 +-
.github/copilot-instructions.md | 119 +-
AISKU/README.md | 1 +
AISKU/Tests/Manual/README.md | 262 ++
AISKU/Tests/Manual/span-e2e-manual-test.html | 818 ++++
AISKU/Tests/Perf/src/AISKUPerf.Tests.ts | 6 +-
AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 18 +-
AISKU/Tests/Unit/src/CdnPackaging.tests.ts | 2 +-
AISKU/Tests/Unit/src/CdnThrottle.tests.ts | 2 +-
.../Tests/Unit/src/IAnalyticsConfig.Tests.ts | 2 +-
.../Tests/Unit/src/NonRecordingSpan.Tests.ts | 773 ++++
AISKU/Tests/Unit/src/OTelInit.Tests.ts | 112 +
.../Unit/src/SnippetInitialization.Tests.ts | 25 +-
.../Unit/src/SpanContextPropagation.Tests.ts | 733 ++++
AISKU/Tests/Unit/src/SpanE2E.Tests.ts | 751 ++++
.../Tests/Unit/src/SpanErrorHandling.Tests.ts | 768 ++++
AISKU/Tests/Unit/src/SpanHelperUtils.Tests.ts | 992 +++++
AISKU/Tests/Unit/src/SpanLifeCycle.Tests.ts | 655 ++++
.../Unit/src/SpanPluginIntegration.Tests.ts | 1020 +++++
AISKU/Tests/Unit/src/SpanUtils.Tests.ts | 1971 ++++++++++
AISKU/Tests/Unit/src/StartSpan.Tests.ts | 301 ++
.../Unit/src/TelemetryItemGeneration.Tests.ts | 831 ++++
.../Unit/src/ThrottleSentMessage.tests.ts | 2 +-
AISKU/Tests/Unit/src/TraceContext.Tests.ts | 735 ++++
AISKU/Tests/Unit/src/TraceProvider.Tests.ts | 716 ++++
.../Tests/Unit/src/TraceSuppression.Tests.ts | 706 ++++
AISKU/Tests/Unit/src/UseSpan.Tests.ts | 1164 ++++++
AISKU/Tests/Unit/src/WithSpan.Tests.ts | 1114 ++++++
AISKU/Tests/Unit/src/aiskuunittests.ts | 51 +-
.../applicationinsights.e2e.fetch.tests.ts | 2 +-
.../Unit/src/applicationinsights.e2e.tests.ts | 443 ++-
AISKU/Tests/Unit/src/sanitizer.e2e.tests.ts | 2 +-
AISKU/Tests/Unit/src/sender.e2e.tests.ts | 2 +-
AISKU/Tests/Unit/src/validate.e2e.tests.ts | 2 +-
.../Tests/es6-module-type-check/package.json | 2 +-
AISKU/Tests/es6-module-type-check/src/main.ts | 2 +-
AISKU/examples/span-usage-example.ts | 68 +
AISKU/package.json | 1 -
AISKU/scripts/publishAzReleaseToCdn.ps1 | 2 +-
AISKU/scripts/setAzActiveCdnVersion.ps1 | 2 +-
AISKU/src/AISku.ts | 243 +-
AISKU/src/IApplicationInsights.ts | 24 +-
AISKU/src/Init.ts | 5 +-
AISKU/src/InternalConstants.ts | 2 +
AISKU/src/Snippet.ts | 3 +-
AISKU/src/applicationinsights-web.ts | 12 +-
AISKU/src/internal/trace/spanUtils.ts | 506 +++
.../Tests/Unit/src/AISKULightSize.Tests.ts | 8 +-
.../Tests/Unit/src/aiskuliteunittests.ts | 2 +
AISKULight/Tests/Unit/src/config.tests.ts | 2 +-
.../Tests/Unit/src/dynamicconfig.tests.ts | 2 +-
.../Tests/Unit/src/otelNegative.tests.ts | 189 +
AISKULight/Tests/UnitTests.html | 6 +-
AISKULight/package.json | 1 -
AISKULight/src/index.ts | 15 +-
README.md | 22 +
RELEASES.md | 118 +-
channels/1ds-post-js/src/ClockSkewManager.ts | 176 +-
channels/1ds-post-js/src/HttpManager.ts | 12 +-
channels/1ds-post-js/src/KillSwitch.ts | 114 +-
.../test/Unit/src/HttpManagerTest.ts | 7 +-
.../test/Unit/src/KillSwitchTest.ts | 18 +-
.../test/Unit/src/PostChannelTest.ts | 5 +
channels/1ds-post-js/test/UnitTests.html | 6 +-
.../Tests/Unit/src/Sample.tests.ts | 23 +-
.../Tests/Unit/src/Sender.tests.ts | 319 +-
.../Tests/Unit/src/StatsBeat.tests.ts | 4 +-
.../package.json | 1 -
.../src/EnvelopeCreator.ts | 87 +-
.../src/Interfaces.ts | 3 +-
.../src/Interfaces/Contracts/IRequestData.ts | 60 +
.../src/SendBuffer.ts | 3 +-
.../src/Sender.ts | 63 +-
.../src/Serializer.ts | 290 +-
.../src/Telemetry/Common/Data.ts | 15 +
.../src/Telemetry/RemoteDependencyData.ts | 91 +
.../src/Telemetry/RequestData.ts | 40 +
.../src/TelemetryProcessors/Sample.ts | 67 +-
.../HashCodeScoreGenerator.ts | 24 +-
.../SamplingScoreGenerator.ts | 30 +-
.../Tests/Unit/src/TestHelper.ts | 2 +-
.../Tests/Unit/src/channel.tests.ts | 2 +-
.../Tests/Unit/src/customprovider.tests.ts | 2 +-
.../Tests/Unit/src/dbprovider.tests.ts | 2 +-
.../Unit/src/offlinebatchhandler.tests.ts | 2 +-
.../Tests/Unit/src/offlinetimer.tests.ts | 2 +-
.../Tests/Unit/src/webprovider.tests.ts | 2 +-
channels/offline-channel-js/package.json | 1 -
.../offline-channel-js/src/Helpers/Utils.ts | 3 +-
.../src/Interfaces/IInMemoryBatch.ts | 3 +-
.../src/Interfaces/IOfflineBatch.ts | 3 +-
.../src/Interfaces/IOfflineProvider.ts | 3 +-
.../offline-channel-js/src/OfflineChannel.ts | 14 +-
channels/offline-channel-js/src/Sender.ts | 10 +-
channels/tee-channel-js/package.json | 1 -
channels/tee-channel-js/src/TeeChannel.ts | 3 +-
common/Tests/Framework/src/AITestClass.ts | 30 +-
common/Tests/Framework/src/Assert.ts | 10 +
common/Tests/Framework/src/TestHelper.ts | 2 +-
common/config/rush/npm-shrinkwrap.json | 117 +-
docs/OTel/README.md | 434 +++
docs/OTel/examples.md | 1289 +++++++
docs/OTel/otelApi.md | 513 +++
docs/OTel/startActiveSpan.md | 962 +++++
docs/OTel/traceApi.md | 809 ++++
docs/OTel/useSpan.md | 804 ++++
docs/OTel/withSpan.md | 725 ++++
docs/README.md | 1 +
docs/upgrade/MergeCommonToCore.md | 114 +
examples/AISKU/src/utils.ts | 3 +-
examples/dependency/src/appinsights-init.ts | 198 +-
.../src/dependencies-example-index.ts | 4 +-
examples/dependency/src/startSpan-example.ts | 377 ++
examples/startSpan/package.json | 62 +
examples/startSpan/rollup.config.js | 100 +
examples/startSpan/src/startSpanExample.ts | 196 +
.../startSpan/src/startspan-example-index.ts | 8 +
examples/startSpan/tsconfig.json | 29 +
.../Unit/src/AnalyticsExtensionSize.tests.ts | 2 +-
.../Tests/Unit/src/AnalyticsPlugin.tests.ts | 38 +-
.../Unit/src/TelemetryItemCreator.tests.ts | 50 +-
.../package.json | 1 -
.../src/JavaScriptSDK/AnalyticsPlugin.ts | 189 +-
.../Interfaces/IAnalyticsConfig.ts | 32 +
.../Telemetry/PageViewManager.ts | 468 +--
.../Telemetry/PageViewPerformanceManager.ts | 239 +-
.../Telemetry/PageVisitTimeManager.ts | 215 +-
.../src/JavaScriptSDK/Timing.ts | 45 +-
.../Tests/Unit/src/cfgsynchelper.tests.ts | 27 +-
.../Tests/Unit/src/cfgsyncplugin.tests.ts | 4 +-
.../package.json | 1 -
.../src/CfgSyncHelperFuncs.ts | 3 +-
.../src/CfgSyncPlugin.ts | 3 +-
.../src/Interfaces/ICfgSyncConfig.ts | 3 +-
.../src/Interfaces/ICfgSyncPlugin.ts | 5 +-
.../Tests/Unit/src/ClickEventTest.ts | 2 +-
.../package.json | 1 -
.../scripts/publishAzReleaseToCdn.ps1 | 2 +-
.../scripts/setAzActiveCdnVersion.ps1 | 2 +-
.../src/ClickAnalyticsPlugin.ts | 10 +-
.../src/Interfaces/Datamodel.ts | 3 +-
.../src/events/PageAction.ts | 3 +-
.../package.json | 1 -
.../scripts/publishAzReleaseToCdn.ps1 | 2 +-
.../scripts/setAzActiveCdnVersion.ps1 | 2 +-
.../Tests/Unit/src/TestChannelPlugin.ts | 54 +
.../Unit/src/W3CTraceStateDependency.tests.ts | 989 +++++
.../Tests/Unit/src/ajax.tests.ts | 413 +-
.../Tests/Unit/src/dependencies.tests.ts | 2 +
.../package.json | 1 -
.../src/DependencyInitializer.ts | 2 +-
.../src/DependencyListener.ts | 9 +-
.../src/InternalConstants.ts | 1 +
.../src/ajax.ts | 435 ++-
.../src/ajaxRecord.ts | 410 +-
.../src/ajaxUtils.ts | 23 -
.../applicationinsights-dependencies-js.ts | 5 +-
.../package.json | 1 -
.../src/OsPlugin.ts | 8 +-
.../scripts/publishAzReleaseToCdn.ps1 | 2 +-
.../scripts/setAzActiveCdnVersion.ps1 | 2 +-
.../Tests/Unit/src/TelemetryContext.Tests.ts | 9 +-
.../Tests/Unit/src/properties.tests.ts | 31 +-
.../package.json | 1 -
.../src/Context/Application.ts | 2 +-
.../src/Context/Device.ts | 2 +-
.../src/Context/Internal.ts | 5 +-
.../src/Context/Location.ts | 2 +-
.../src/Context/Session.ts | 8 +-
.../src/Context/TelemetryTrace.ts | 29 -
.../src/Context/User.ts | 5 +-
.../src/Interfaces/IPropTelemetryContext.ts | 3 +-
.../src/PropertiesPlugin.ts | 35 +-
.../src/TelemetryContext.ts | 172 +-
.../src/applicationinsights-properties-js.ts | 5 +-
gruntfile.js | 13 +-
package.json | 2 +-
rush.json | 5 +
shared/1ds-core-js/src/AppInsightsCore.ts | 5 +-
shared/1ds-core-js/src/DataModels.ts | 12 +-
shared/1ds-core-js/src/Index.ts | 6 +-
shared/1ds-core-js/src/InternalConstants.ts | 2 +
shared/1ds-core-js/src/extSpanUtils.ts | 503 +++
.../test/Unit/src/FileSizeCheckTest.ts | 8 +-
.../test/Unit/src/SpanUtilsTests.ts | 765 ++++
.../test/Unit/src/core.unittests.ts | 2 +
shared/1ds-core-js/test/UnitTests.html | 6 +-
shared/AppInsightsCommon/README.md | 23 +-
.../Unit/src/appinsights-common.tests.ts | 19 -
shared/AppInsightsCommon/Tests/UnitTests.html | 44 -
shared/AppInsightsCommon/Tests/tsconfig.json | 13 -
shared/AppInsightsCommon/package.json | 5 +-
shared/AppInsightsCommon/src/Enums.ts | 79 -
shared/AppInsightsCommon/src/HelperFuncs.ts | 55 -
.../src/Interfaces/Contracts/IEnvelope.ts | 50 -
.../src/Interfaces/Contracts/RequestData.ts | 50 -
.../src/Interfaces/ICorrelationConfig.ts | 100 -
.../src/Interfaces/PartAExtensions.ts | 14 -
.../src/applicationinsights-common.ts | 195 +-
shared/AppInsightsCore/README.md | 11 +
.../SpanImplementationSummary.md | 100 +
...Span_Implementation_Refactoring_Summary.md | 126 +
.../Tests/Unit/src/GlobalTestHooks.Test.ts | 13 -
.../src/OpenTelemetry/commonUtils.Tests.ts | 1031 +++++
.../Unit/src/OpenTelemetry/errors.Tests.ts | 680 ++++
.../src/OpenTelemetry/otelNegative.Tests.ts | 690 ++++
.../Unit/src/ai}/AppInsightsCommon.tests.ts | 5 +-
.../src/{ => ai}/AppInsightsCoreSize.Tests.ts | 14 +-
.../{ => ai}/ApplicationInsightsCore.Tests.ts | 19 +-
.../src/ai}/ConnectionStringParser.tests.ts | 4 +-
.../Unit/src/{ => ai}/CookieManager.Tests.ts | 8 +-
.../Unit/src/{ => ai}/EventHelper.Tests.ts | 18 +-
.../{ => ai}/EventsDiscardedReason.Tests.ts | 2 +-
.../Tests/Unit/src/ai}/Exception.tests.ts | 13 +-
.../Unit/src/ai}/GlobalTestHooks.Test.ts | 2 +-
.../Unit/src/{ => ai}/HelperFunc.Tests.ts | 10 +-
.../Unit/src/{ => ai}/LoggingEnum.Tests.ts | 4 +-
.../Unit/src/ai}/RequestHeaders.tests.ts | 4 +-
.../src/{ => ai}/SendPostManager.Tests.ts | 8 +-
.../Tests/Unit/src/ai}/SeverityLevel.tests.ts | 4 +-
.../Unit/src/{ => ai}/StatsBeat.Tests.ts | 22 +-
.../Tests/Unit/src/{ => ai}/TestPlugins.ts | 24 +-
.../Tests/Unit/src/ai}/ThrottleMgr.tests.ts | 16 +-
.../Unit/src/{ => ai}/UpdateConfig.Tests.ts | 8 +-
.../Tests/Unit/src/ai}/Util.tests.ts | 24 +-
.../Tests/Unit/src/aiunittests.ts | 66 +-
.../src/attribute/attributeContainer.Tests.ts | 3412 +++++++++++++++++
.../Unit/src/{ => config}/Dynamic.Tests.ts | 22 +-
.../src/{ => config}/DynamicConfig.Tests.ts | 32 +-
.../src/trace/W3CTraceStateModes.tests.ts | 224 ++
.../src/{ => trace}/W3cTraceParentTests.ts | 6 +-
.../Unit/src/trace/W3cTraceState.Tests.ts | 1606 ++++++++
.../Tests/Unit/src/trace/span.Tests.ts | 1970 ++++++++++
.../Tests/Unit/src/trace/traceState.Tests.ts | 448 +++
.../Tests/Unit/src/trace/traceUtils.Tests.ts | 1355 +++++++
shared/AppInsightsCore/Tests/tsconfig.json | 2 +-
shared/AppInsightsCore/api-extractor.json | 2 +-
shared/AppInsightsCore/package.json | 4 +-
shared/AppInsightsCore/rollup.config.js | 43 +-
.../IDistributedTraceContext.ts | 54 -
.../src/JavaScriptSDK/Constants.ts | 14 -
.../src/JavaScriptSDK/TelemetryHelpers.ts | 183 -
.../src/applicationinsights-core-js.ts | 112 -
.../ConfigDefaultHelpers.ts | 8 +-
.../src/{Config => config}/ConfigDefaults.ts | 4 +-
.../src/{Config => config}/DynamicConfig.ts | 20 +-
.../src/{Config => config}/DynamicProperty.ts | 10 +-
.../src/{Config => config}/DynamicState.ts | 10 +-
.../src/{Config => config}/DynamicSupport.ts | 9 +-
.../src/constants}/Constants.ts | 4 +
.../InternalConstants.ts | 0
.../AggregationError.ts | 0
.../AppInsightsCore.ts | 420 +-
.../src/{JavaScriptSDK => core}/AsyncUtils.ts | 0
.../BaseTelemetryPlugin.ts | 34 +-
.../src/{JavaScriptSDK => core}/CookieMgr.ts | 36 +-
.../DbgExtensionUtils.ts | 8 +-
.../InstrumentHooks.ts | 4 +-
.../NotificationManager.ts | 17 +-
.../{JavaScriptSDK => core}/PerfManager.ts | 11 +-
.../ProcessTelemetryContext.ts | 172 +-
.../ResponseHelpers.ts | 13 +-
.../SenderPostManager.ts | 40 +-
.../src/{JavaScriptSDK => core}/StatsBeat.ts | 26 +-
.../src/core/TelemetryHelpers.ts | 374 ++
.../TelemetryInitializerPlugin.ts | 18 +-
.../UnloadHandlerContainer.ts | 9 +-
.../UnloadHookContainer.ts | 8 +-
.../DiagnosticLogger.ts | 30 +-
.../src/diagnostics}/ThrottleMgr.ts | 28 +-
.../EnumHelperFuncs.ts | 0
.../src/enums/W3CTraceFlags.ts | 18 +
.../src/enums/ai/DependencyTypes.ts | 50 +
shared/AppInsightsCore/src/enums/ai/Enums.ts | 149 +
.../ai}/EventsDiscardedReason.ts | 2 +-
.../ai}/FeatureOptInEnums.ts | 3 +
.../ai}/InitActiveStatusEnum.ts | 5 +-
.../ai}/LoggingEnums.ts | 7 +-
.../ai}/SendRequestReason.ts | 0
.../ai}/StatsType.ts | 0
.../ai}/TelemetryUnloadReason.ts | 0
.../ai}/TelemetryUpdateReason.ts | 0
.../src/enums/ai/TraceHeadersMode.ts | 29 +
.../src/enums/otel/OTelSamplingDecision.ts | 33 +
.../src/enums/otel/OTelSpanKind.ts | 53 +
.../src/enums/otel/OTelSpanStatus.ts | 34 +
.../src/enums/otel/eAttributeChangeOp.ts | 46 +
shared/AppInsightsCore/src/index.ts | 370 ++
.../src/interfaces/IException.ts | 37 +
.../src/interfaces/IOTelHrTime.ts | 81 +
.../src/interfaces/ai}/ConnectionString.ts | 3 +
.../src/interfaces/ai}/IAppInsights.ts | 3 +-
.../ai}/IAppInsightsCore.ts | 33 +-
.../ai}/IChannelControls.ts | 2 +-
.../ai}/IChannelControlsHost.ts | 0
.../src/interfaces/ai}/IConfig.ts | 6 +-
.../ai}/IConfiguration.ts | 14 +-
.../ai}/ICookieMgr.ts | 0
.../src/interfaces/ai/ICorrelationConfig.ts | 192 +
.../ai}/IDbgExtension.ts | 0
.../interfaces/ai}/IDependencyTelemetry.ts | 0
.../ai}/IDiagnosticLogger.ts | 19 +-
.../interfaces/ai/IDistributedTraceContext.ts | 334 ++
.../src/interfaces/ai}/IEventTelemetry.ts | 4 +-
.../ai}/IExceptionConfig.ts | 0
.../src/interfaces/ai}/IExceptionTelemetry.ts | 2 +-
.../ai}/IFeatureOptIn.ts | 9 +-
.../ai}/IInstrumentHooks.ts | 0
.../src/interfaces/ai}/IMetricTelemetry.ts | 3 +
.../ai}/INetworkStatsbeat.ts | 0
.../ai}/INotificationListener.ts | 0
.../ai}/INotificationManager.ts | 4 +-
.../ai}/IPageViewPerformanceTelemetry.ts | 4 +-
.../src/interfaces/ai}/IPageViewTelemetry.ts | 3 +
.../src/interfaces/ai}/IPartC.ts | 0
.../ai}/IPerfEvent.ts | 0
.../ai}/IPerfManager.ts | 2 +-
.../ai}/IProcessTelemetryContext.ts | 9 +-
.../src/interfaces/ai}/IPropertiesPlugin.ts | 0
.../src/interfaces/ai}/IRequestContext.ts | 3 +
.../src/interfaces/ai/IRequestTelemetry.ts | 44 +
.../ai}/ISenderPostManager.ts | 5 +-
.../ai}/IStatsBeat.ts | 2 +-
.../ai}/IStatsEventData.ts | 0
.../ai}/IStatsMgr.ts | 0
.../src/interfaces/ai}/IStorageBuffer.ts | 5 +-
.../src/interfaces/ai}/ITelemetryContext.ts | 22 +-
.../ai}/ITelemetryInitializers.ts | 3 +
.../ai}/ITelemetryItem.ts | 0
.../ai}/ITelemetryPlugin.ts | 0
.../ai}/ITelemetryPluginChain.ts | 0
.../ai}/ITelemetryUnloadState.ts | 2 +-
.../ai}/ITelemetryUpdateState.ts | 2 +-
.../src/interfaces/ai}/IThrottleMgr.ts | 2 +
.../ai}/ITraceParent.ts | 0
.../src/interfaces/ai/ITraceProvider.ts | 157 +
.../src/interfaces/ai}/ITraceTelemetry.ts | 2 +-
.../ai}/IUnloadHook.ts | 0
.../ai}/IUnloadableComponent.ts | 0
.../src/interfaces/ai/IW3cTraceState.ts | 78 +
.../ai}/IXDomainRequest.ts | 3 +
.../ai}/IXHROverride.ts | 12 +-
.../src/interfaces/ai/PartAExtensions.ts | 17 +
.../interfaces/ai/context}/IApplication.ts | 0
.../src/interfaces/ai/context}/IDevice.ts | 0
.../src/interfaces/ai/context}/IInternal.ts | 0
.../src/interfaces/ai/context}/ILocation.ts | 0
.../ai/context}/IOperatingSystem.ts | 0
.../src/interfaces/ai/context}/ISample.ts | 2 +-
.../src/interfaces/ai/context}/ISession.ts | 0
.../interfaces/ai/context}/ISessionManager.ts | 0
.../interfaces/ai/context}/ITelemetryTrace.ts | 14 +-
.../src/interfaces/ai/context}/IUser.ts | 0
.../src/interfaces/ai/context}/IWeb.ts | 0
.../ai/contracts}/AvailabilityData.ts | 4 +-
.../ai/contracts}/ContextTagKeys.ts | 2 +-
.../interfaces/ai/contracts}/DataPointType.ts | 0
.../ai/contracts}/DependencyKind.ts | 0
.../ai/contracts}/DependencySourceType.ts | 0
.../src/interfaces/ai/contracts}/IBase.ts | 0
.../src/interfaces/ai/contracts}/IData.ts | 0
.../interfaces/ai/contracts}/IDataPoint.ts | 0
.../src/interfaces/ai/contracts}/IDomain.ts | 0
.../interfaces/ai/contracts}/IEventData.ts | 0
.../ai/contracts}/IExceptionData.ts | 0
.../ai/contracts}/IExceptionDetails.ts | 0
.../interfaces/ai/contracts}/IMessageData.ts | 0
.../interfaces/ai/contracts}/IMetricData.ts | 0
.../interfaces/ai/contracts}/IPageViewData.ts | 0
.../ai/contracts}/IPageViewPerfData.ts | 0
.../ai/contracts}/IRemoteDependencyData.ts | 0
.../interfaces/ai/contracts}/IStackFrame.ts | 0
.../interfaces/ai/contracts}/SeverityLevel.ts | 2 +-
.../src/interfaces/ai/telemetry}/IEnvelope.ts | 0
.../interfaces/ai/telemetry}/ISerializable.ts | 4 +-
.../config}/IConfigDefaults.ts | 2 +-
.../config}/IDynamicConfigHandler.ts | 6 +-
.../config}/IDynamicPropertyHandler.ts | 4 +-
.../config}/IDynamicWatcher.ts | 4 +-
.../config}/_IDynamicConfigHandlerState.ts | 3 +
.../src/interfaces/otel/IOTelApi.ts | 58 +
.../src/interfaces/otel/IOTelApiCtx.ts | 18 +
.../src/interfaces/otel/IOTelAttributes.ts | 36 +
.../otel/attribute/IAttributeContainer.ts | 191 +
.../otel/config/IOTelAttributeLimits.ts | 72 +
.../src/interfaces/otel/config/IOTelConfig.ts | 55 +
.../otel/config/IOTelErrorHandlers.ts | 196 +
.../interfaces/otel/config/IOTelSpanLimits.ts | 131 +
.../interfaces/otel/config/IOTelTraceCfg.ts | 85 +
.../src/interfaces/otel/trace/IOTelSpan.ts | 488 +++
.../interfaces/otel/trace/IOTelSpanContext.ts | 23 +
.../src/interfaces/otel/trace/IOTelSpanCtx.ts | 75 +
.../interfaces/otel/trace/IOTelSpanOptions.ts | 34 +
.../interfaces/otel/trace/IOTelSpanStatus.ts | 16 +
.../interfaces/otel/trace/IOTelTraceApi.ts | 51 +
.../interfaces/otel/trace/IOTelTraceState.ts | 62 +
.../src/interfaces/otel/trace/IOTelTracer.ts | 147 +
.../otel/trace/IOTelTracerOptions.ts | 9 +
.../otel/trace/IOTelTracerProvider.ts | 39 +
.../interfaces/otel/trace/IReadableSpan.ts | 56 +
.../EventHelpers.ts | 69 +-
.../src/internal/attributeHelpers.ts | 93 +
.../src/internal/commonUtils.ts | 329 ++
.../src/internal/handleErrors.ts | 107 +
.../src/internal/noopHelpers.ts | 16 +
.../src/internal/timeHelpers.ts | 371 ++
.../AppInsightsCore/src/otel/api/OTelApi.ts | 25 +
.../src/otel/api/errors/OTelError.ts | 32 +
.../api/errors/OTelInvalidAttributeError.ts | 34 +
.../src/otel/api/errors/OTelSpanError.ts | 31 +
.../src/otel/api/trace/span.ts | 290 ++
.../src/otel/api/trace/spanContext.ts | 65 +
.../src/otel/api/trace/traceApi.ts | 46 +
.../src/otel/api/trace/traceProvider.ts | 76 +
.../src/otel/api/trace/traceState.ts | 109 +
.../src/otel/api/trace/tracer.ts | 52 +
.../src/otel/api/trace/tracerProvider.ts | 40 +
.../src/otel/api/trace/utils.ts | 383 ++
.../src/otel/attribute/SemanticConventions.ts | 106 +
.../src/otel/attribute/attributeContainer.ts | 1136 ++++++
.../src/telemetry}/ConnectionStringParser.ts | 6 +-
.../src/telemetry}/RequestResponseHeaders.ts | 10 +-
.../src/telemetry}/TelemetryItemCreator.ts | 17 +-
.../src/telemetry/W3cTraceState.ts | 458 +++
.../src/telemetry/ai}/Common/Data.ts | 10 +-
.../src/telemetry/ai}/Common/DataPoint.ts | 8 +-
.../src/telemetry/ai}/Common/DataSanitizer.ts | 26 +-
.../src/telemetry/ai}/Common/Envelope.ts | 11 +-
.../src/telemetry/ai/DataTypes.ts | 11 +
.../src/telemetry/ai/EnvelopeTypes.ts | 16 +
.../src/telemetry/ai}/Event.ts | 23 +-
.../src/telemetry/ai}/Exception.ts | 49 +-
.../src/telemetry/ai}/Metric.ts | 22 +-
.../src/telemetry/ai}/PageView.ts | 24 +-
.../src/telemetry/ai}/PageViewPerformance.ts | 24 +-
.../src/telemetry/ai}/RemoteDependencyData.ts | 30 +-
.../src/telemetry/ai}/Trace.ts | 24 +-
.../src/{JavaScriptSDK => utils}/CoreUtils.ts | 2 +-
.../DataCacheHelper.ts | 2 +-
.../src/utils}/DomHelperFuncs.ts | 3 +-
.../src/{JavaScriptSDK => utils}/EnvUtils.ts | 165 +-
.../{JavaScriptSDK => utils}/HelperFuncs.ts | 225 +-
.../src/utils}/Offline.ts | 13 +-
.../{JavaScriptSDK => utils}/RandomHelper.ts | 12 +-
.../src/utils}/StorageHelperFuncs.ts | 16 +-
.../TraceParent.ts} | 36 +-
.../src/utils}/UrlHelperFuncs.ts | 17 +-
.../src => AppInsightsCore/src/utils}/Util.ts | 65 +-
shared/AppInsightsCore/tsconfig.json | 10 +-
shared/AppInsightsCore/typedoc.json | 2 +-
.../package.json | 2 +-
.../src/aiSupport.ts | 2 +-
.../src/snippet.ts | 2 +-
.../src/type.ts | 2 +-
tools/chrome-debug-extension/package.json | 1 -
.../scripts/publishAzReleaseToCdn.ps1 | 2 +-
.../scripts/setAzActiveCdnVersion.ps1 | 2 +-
.../src/dataSources/dataSources.ts | 2 +-
.../config/scripts/publishAzReleaseToCdn.ps1 | 2 +-
.../config/scripts/setAzActiveCdnVersion.ps1 | 2 +-
tools/grunt-tasks/minifyNames.js | 9 +-
tools/grunt-tasks/qunit.js | 52 +-
tools/release-tools/setVersion.js | 2 +-
tools/rollup-es5/Tests/UnitTests.html | 36 +-
tools/rollup-es5/src/ImportCheck.ts | 5 +-
tools/shims/Tests/UnitTests.html | 2 +-
tsdoc.json | 10 +
version.json | 6 +-
468 files changed, 49174 insertions(+), 3932 deletions(-)
create mode 100644 AISKU/Tests/Manual/README.md
create mode 100644 AISKU/Tests/Manual/span-e2e-manual-test.html
create mode 100644 AISKU/Tests/Unit/src/NonRecordingSpan.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/OTelInit.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanContextPropagation.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanE2E.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanErrorHandling.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanHelperUtils.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanLifeCycle.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanPluginIntegration.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/SpanUtils.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/StartSpan.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/TelemetryItemGeneration.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/TraceContext.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/TraceProvider.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/TraceSuppression.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/UseSpan.Tests.ts
create mode 100644 AISKU/Tests/Unit/src/WithSpan.Tests.ts
create mode 100644 AISKU/examples/span-usage-example.ts
create mode 100644 AISKU/src/internal/trace/spanUtils.ts
create mode 100644 AISKULight/Tests/Unit/src/otelNegative.tests.ts
create mode 100644 channels/applicationinsights-channel-js/src/Interfaces/Contracts/IRequestData.ts
create mode 100644 channels/applicationinsights-channel-js/src/Telemetry/Common/Data.ts
create mode 100644 channels/applicationinsights-channel-js/src/Telemetry/RemoteDependencyData.ts
create mode 100644 channels/applicationinsights-channel-js/src/Telemetry/RequestData.ts
create mode 100644 docs/OTel/README.md
create mode 100644 docs/OTel/examples.md
create mode 100644 docs/OTel/otelApi.md
create mode 100644 docs/OTel/startActiveSpan.md
create mode 100644 docs/OTel/traceApi.md
create mode 100644 docs/OTel/useSpan.md
create mode 100644 docs/OTel/withSpan.md
create mode 100644 docs/upgrade/MergeCommonToCore.md
create mode 100644 examples/dependency/src/startSpan-example.ts
create mode 100644 examples/startSpan/package.json
create mode 100644 examples/startSpan/rollup.config.js
create mode 100644 examples/startSpan/src/startSpanExample.ts
create mode 100644 examples/startSpan/src/startspan-example-index.ts
create mode 100644 examples/startSpan/tsconfig.json
create mode 100644 extensions/applicationinsights-dependencies-js/Tests/Unit/src/TestChannelPlugin.ts
create mode 100644 extensions/applicationinsights-dependencies-js/Tests/Unit/src/W3CTraceStateDependency.tests.ts
delete mode 100644 extensions/applicationinsights-dependencies-js/src/ajaxUtils.ts
delete mode 100644 extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts
create mode 100644 shared/1ds-core-js/src/extSpanUtils.ts
create mode 100644 shared/1ds-core-js/test/Unit/src/SpanUtilsTests.ts
delete mode 100644 shared/AppInsightsCommon/Tests/Unit/src/appinsights-common.tests.ts
delete mode 100644 shared/AppInsightsCommon/Tests/UnitTests.html
delete mode 100644 shared/AppInsightsCommon/Tests/tsconfig.json
delete mode 100644 shared/AppInsightsCommon/src/Enums.ts
delete mode 100644 shared/AppInsightsCommon/src/HelperFuncs.ts
delete mode 100644 shared/AppInsightsCommon/src/Interfaces/Contracts/IEnvelope.ts
delete mode 100644 shared/AppInsightsCommon/src/Interfaces/Contracts/RequestData.ts
delete mode 100644 shared/AppInsightsCommon/src/Interfaces/ICorrelationConfig.ts
delete mode 100644 shared/AppInsightsCommon/src/Interfaces/PartAExtensions.ts
create mode 100644 shared/AppInsightsCore/SpanImplementationSummary.md
create mode 100644 shared/AppInsightsCore/Span_Implementation_Refactoring_Summary.md
delete mode 100644 shared/AppInsightsCore/Tests/Unit/src/GlobalTestHooks.Test.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/OpenTelemetry/commonUtils.Tests.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/OpenTelemetry/errors.Tests.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/OpenTelemetry/otelNegative.Tests.ts
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/AppInsightsCommon.tests.ts (98%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/AppInsightsCoreSize.Tests.ts (94%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/ApplicationInsightsCore.Tests.ts (99%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/ConnectionStringParser.tests.ts (97%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/CookieManager.Tests.ts (99%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/EventHelper.Tests.ts (96%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/EventsDiscardedReason.Tests.ts (99%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/Exception.tests.ts (98%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/GlobalTestHooks.Test.ts (82%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/HelperFunc.Tests.ts (98%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/LoggingEnum.Tests.ts (98%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/RequestHeaders.tests.ts (97%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/SendPostManager.Tests.ts (97%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/SeverityLevel.tests.ts (97%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/StatsBeat.Tests.ts (94%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/TestPlugins.ts (84%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/ThrottleMgr.tests.ts (99%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => ai}/UpdateConfig.Tests.ts (98%)
rename shared/{AppInsightsCommon/Tests/Unit/src => AppInsightsCore/Tests/Unit/src/ai}/Util.tests.ts (98%)
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/attribute/attributeContainer.Tests.ts
rename shared/AppInsightsCore/Tests/Unit/src/{ => config}/Dynamic.Tests.ts (98%)
rename shared/AppInsightsCore/Tests/Unit/src/{ => config}/DynamicConfig.Tests.ts (98%)
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/trace/W3CTraceStateModes.tests.ts
rename shared/AppInsightsCore/Tests/Unit/src/{ => trace}/W3cTraceParentTests.ts (98%)
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/trace/W3cTraceState.Tests.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/trace/span.Tests.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/trace/traceState.Tests.ts
create mode 100644 shared/AppInsightsCore/Tests/Unit/src/trace/traceUtils.Tests.ts
delete mode 100644 shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IDistributedTraceContext.ts
delete mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/Constants.ts
delete mode 100644 shared/AppInsightsCore/src/JavaScriptSDK/TelemetryHelpers.ts
delete mode 100644 shared/AppInsightsCore/src/applicationinsights-core-js.ts
rename shared/AppInsightsCore/src/{Config => config}/ConfigDefaultHelpers.ts (93%)
rename shared/AppInsightsCore/src/{Config => config}/ConfigDefaults.ts (98%)
rename shared/AppInsightsCore/src/{Config => config}/DynamicConfig.ts (91%)
rename shared/AppInsightsCore/src/{Config => config}/DynamicProperty.ts (96%)
rename shared/AppInsightsCore/src/{Config => config}/DynamicState.ts (94%)
rename shared/AppInsightsCore/src/{Config => config}/DynamicSupport.ts (94%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/constants}/Constants.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => constants}/InternalConstants.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/AggregationError.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/AppInsightsCore.ts (79%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/AsyncUtils.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/BaseTelemetryPlugin.ts (92%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/CookieMgr.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/DbgExtensionUtils.ts (84%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/InstrumentHooks.ts (99%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/NotificationManager.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/PerfManager.ts (96%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/ProcessTelemetryContext.ts (78%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/ResponseHelpers.ts (68%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/SenderPostManager.ts (96%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/StatsBeat.ts (94%)
create mode 100644 shared/AppInsightsCore/src/core/TelemetryHelpers.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/TelemetryInitializerPlugin.ts (87%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/UnloadHandlerContainer.ts (79%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => core}/UnloadHookContainer.ts (90%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => diagnostics}/DiagnosticLogger.ts (93%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/diagnostics}/ThrottleMgr.ts (94%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums}/EnumHelperFuncs.ts (100%)
create mode 100644 shared/AppInsightsCore/src/enums/W3CTraceFlags.ts
create mode 100644 shared/AppInsightsCore/src/enums/ai/DependencyTypes.ts
create mode 100644 shared/AppInsightsCore/src/enums/ai/Enums.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/EventsDiscardedReason.ts (97%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/FeatureOptInEnums.ts (86%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/InitActiveStatusEnum.ts (84%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/LoggingEnums.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/SendRequestReason.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/StatsType.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/TelemetryUnloadReason.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Enums => enums/ai}/TelemetryUpdateReason.ts (100%)
create mode 100644 shared/AppInsightsCore/src/enums/ai/TraceHeadersMode.ts
create mode 100644 shared/AppInsightsCore/src/enums/otel/OTelSamplingDecision.ts
create mode 100644 shared/AppInsightsCore/src/enums/otel/OTelSpanKind.ts
create mode 100644 shared/AppInsightsCore/src/enums/otel/OTelSpanStatus.ts
create mode 100644 shared/AppInsightsCore/src/enums/otel/eAttributeChangeOp.ts
create mode 100644 shared/AppInsightsCore/src/index.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/IException.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/IOTelHrTime.ts
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/ConnectionString.ts (67%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IAppInsights.ts (94%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IAppInsightsCore.ts (92%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IChannelControls.ts (98%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IChannelControlsHost.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IConfig.ts (98%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IConfiguration.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ICookieMgr.ts (100%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/ICorrelationConfig.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IDbgExtension.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IDependencyTelemetry.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IDiagnosticLogger.ts (85%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/IDistributedTraceContext.ts
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IEventTelemetry.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IExceptionConfig.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IExceptionTelemetry.ts (97%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IFeatureOptIn.ts (85%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IInstrumentHooks.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IMetricTelemetry.ts (91%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/INetworkStatsbeat.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/INotificationListener.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/INotificationManager.ts (95%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IPageViewPerformanceTelemetry.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IPageViewTelemetry.ts (94%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IPartC.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IPerfEvent.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IPerfManager.ts (95%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IProcessTelemetryContext.ts (94%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IPropertiesPlugin.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IRequestContext.ts (64%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/IRequestTelemetry.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ISenderPostManager.ts (97%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IStatsBeat.ts (98%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IStatsEventData.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IStatsMgr.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IStorageBuffer.ts (77%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/ITelemetryContext.ts (68%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryInitializers.ts (88%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryItem.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryPlugin.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryPluginChain.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryUnloadState.ts (72%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITelemetryUpdateState.ts (93%)
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/IThrottleMgr.ts (96%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/ITraceParent.ts (100%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/ITraceProvider.ts
rename shared/{AppInsightsCommon/src/Interfaces => AppInsightsCore/src/interfaces/ai}/ITraceTelemetry.ts (89%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IUnloadHook.ts (100%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IUnloadableComponent.ts (100%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/IW3cTraceState.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IXDomainRequest.ts (92%)
rename shared/AppInsightsCore/src/{JavaScriptSDK.Interfaces => interfaces/ai}/IXHROverride.ts (82%)
create mode 100644 shared/AppInsightsCore/src/interfaces/ai/PartAExtensions.ts
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IApplication.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IDevice.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IInternal.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/ILocation.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IOperatingSystem.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/ISample.ts (76%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/ISession.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/ISessionManager.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/ITelemetryTrace.ts (64%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IUser.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Context => AppInsightsCore/src/interfaces/ai/context}/IWeb.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/AvailabilityData.ts (92%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/ContextTagKeys.ts (99%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/DataPointType.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/DependencyKind.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/DependencySourceType.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IBase.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IDataPoint.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IDomain.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IEventData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IExceptionData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IExceptionDetails.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IMessageData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IMetricData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IPageViewData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IPageViewPerfData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IRemoteDependencyData.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/IStackFrame.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Contracts => AppInsightsCore/src/interfaces/ai/contracts}/SeverityLevel.ts (90%)
rename shared/{AppInsightsCommon/src/Interfaces/Telemetry => AppInsightsCore/src/interfaces/ai/telemetry}/IEnvelope.ts (100%)
rename shared/{AppInsightsCommon/src/Interfaces/Telemetry => AppInsightsCore/src/interfaces/ai/telemetry}/ISerializable.ts (73%)
rename shared/AppInsightsCore/src/{Config => interfaces/config}/IConfigDefaults.ts (97%)
rename shared/AppInsightsCore/src/{Config => interfaces/config}/IDynamicConfigHandler.ts (96%)
rename shared/AppInsightsCore/src/{Config => interfaces/config}/IDynamicPropertyHandler.ts (85%)
rename shared/AppInsightsCore/src/{Config => interfaces/config}/IDynamicWatcher.ts (94%)
rename shared/AppInsightsCore/src/{Config => interfaces/config}/_IDynamicConfigHandlerState.ts (95%)
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/IOTelApi.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/IOTelApiCtx.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/IOTelAttributes.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/attribute/IAttributeContainer.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/config/IOTelAttributeLimits.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/config/IOTelConfig.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/config/IOTelErrorHandlers.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/config/IOTelSpanLimits.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/config/IOTelTraceCfg.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelSpan.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelSpanContext.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelSpanCtx.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelSpanOptions.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelSpanStatus.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelTraceApi.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelTraceState.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelTracer.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelTracerOptions.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IOTelTracerProvider.ts
create mode 100644 shared/AppInsightsCore/src/interfaces/otel/trace/IReadableSpan.ts
rename shared/AppInsightsCore/src/{JavaScriptSDK => internal}/EventHelpers.ts (89%)
create mode 100644 shared/AppInsightsCore/src/internal/attributeHelpers.ts
create mode 100644 shared/AppInsightsCore/src/internal/commonUtils.ts
create mode 100644 shared/AppInsightsCore/src/internal/handleErrors.ts
create mode 100644 shared/AppInsightsCore/src/internal/noopHelpers.ts
create mode 100644 shared/AppInsightsCore/src/internal/timeHelpers.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/OTelApi.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/errors/OTelError.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/errors/OTelInvalidAttributeError.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/errors/OTelSpanError.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/span.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/spanContext.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/traceApi.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/traceProvider.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/traceState.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/tracer.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/tracerProvider.ts
create mode 100644 shared/AppInsightsCore/src/otel/api/trace/utils.ts
create mode 100644 shared/AppInsightsCore/src/otel/attribute/SemanticConventions.ts
create mode 100644 shared/AppInsightsCore/src/otel/attribute/attributeContainer.ts
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/telemetry}/ConnectionStringParser.ts (87%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/telemetry}/RequestResponseHeaders.ts (91%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/telemetry}/TelemetryItemCreator.ts (81%)
create mode 100644 shared/AppInsightsCore/src/telemetry/W3cTraceState.ts
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Common/Data.ts (68%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Common/DataPoint.ts (82%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Common/DataSanitizer.ts (88%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Common/Envelope.ts (87%)
create mode 100644 shared/AppInsightsCore/src/telemetry/ai/DataTypes.ts
create mode 100644 shared/AppInsightsCore/src/telemetry/ai/EnvelopeTypes.ts
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Event.ts (65%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Exception.ts (95%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Metric.ts (73%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/PageView.ts (74%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/PageViewPerformance.ts (79%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/RemoteDependencyData.ts (78%)
rename shared/{AppInsightsCommon/src/Telemetry => AppInsightsCore/src/telemetry/ai}/Trace.ts (66%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => utils}/CoreUtils.ts (97%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => utils}/DataCacheHelper.ts (97%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/utils}/DomHelperFuncs.ts (86%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => utils}/EnvUtils.ts (75%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => utils}/HelperFuncs.ts (71%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/utils}/Offline.ts (92%)
rename shared/AppInsightsCore/src/{JavaScriptSDK => utils}/RandomHelper.ts (94%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/utils}/StorageHelperFuncs.ts (92%)
rename shared/AppInsightsCore/src/{JavaScriptSDK/W3cTraceParent.ts => utils/TraceParent.ts} (89%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/utils}/UrlHelperFuncs.ts (85%)
rename shared/{AppInsightsCommon/src => AppInsightsCore/src/utils}/Util.ts (79%)
diff --git a/.aiAutoMinify.json b/.aiAutoMinify.json
index 21ede5e68..8209d565e 100644
--- a/.aiAutoMinify.json
+++ b/.aiAutoMinify.json
@@ -5,6 +5,15 @@
"_eSetDynamicPropertyFlags",
"ePendingOp",
"CallbackType",
+ "eW3CTraceFlags",
+ "eRequestHeaders",
+ "eTraceStateKeyType",
+ "eOfflineValue",
+ "eDependencyTypes",
+ "eStorageType",
+ "FieldType",
+ "eDistributedTracingModes",
+ "EventPersistenceValue",
"eEventsDiscardedReason",
"eBatchDiscardedReason",
"FeatureOptInMode",
@@ -16,27 +25,28 @@
"TransportType",
"eStatsType",
"TelemetryUnloadReason",
- "TelemetryUpdateReason"
- ]
- },
- "@microsoft/applicationinsights-perfmarkmeasure-js": {
- "constEnums": []
- },
- "@microsoft/applicationinsights-common": {
- "constEnums": [
- "eStorageType",
- "FieldType",
- "eDistributedTracingModes",
- "EventPersistenceValue",
- "eOfflineValue",
- "eRequestHeaders",
+ "TelemetryUpdateReason",
+ "eTraceHeadersMode",
+ "eAttributeChangeOp",
+ "eOTelSamplingDecision",
+ "eOTelSpanKind",
+ "eOTelSpanStatusCode",
+ "eAttributeSource",
+ "AddAttributeResult",
"DataPointType",
"DependencyKind",
"DependencySourceType",
"eSeverityLevel",
+ "eAttributeFilter",
"DataSanitizerValues"
]
},
+ "@microsoft/applicationinsights-perfmarkmeasure-js": {
+ "constEnums": []
+ },
+ "@microsoft/applicationinsights-common": {
+ "constEnums": []
+ },
"@microsoft/applicationinsights-properties-js": {
"constEnums": []
},
@@ -47,7 +57,9 @@
"constEnums": []
},
"@microsoft/applicationinsights-channel-js": {
- "constEnums": []
+ "constEnums": [
+ "eSerializeType"
+ ]
},
"@microsoft/applicationinsights-react-native": {
"constEnums": []
@@ -61,7 +73,9 @@
"constEnums": []
},
"@microsoft/applicationinsights-analytics-js": {
- "constEnums": []
+ "constEnums": [
+ "eRouteTraceStrategy"
+ ]
},
"@microsoft/applicationinsights-web": {
"constEnums": []
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 03c797bf0..1f3b0395a 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -188,17 +188,120 @@ export class MyPlugin extends BaseTelemetryPlugin {
- Test both success and failure scenarios
- Verify telemetry data structure and content
+### Testing Framework Requirements
+- **Extend AITestClass**: All test classes must extend `AITestClass` from `@microsoft/ai-test-framework`
+- **Use Framework Tools**: Leverage existing framework helpers like `this.hookFetch()`, `this.useFakeTimers()`, and `this.onDone()`
+- **Proper Registration**: Implement `registerTests()` method and use `this.testCase()` for test registration
+- **Async Tests**: Return `IPromise` from test functions for asynchronous operations (do not use deprecated `testCaseAsync`)
+
+### Critical Cleanup & Resource Management
+- **Mandatory Core Cleanup**: Always call `appInsightsCore.unload(false)` in test cleanup to prevent hook pollution between tests
+- **Extension Teardown**: Only call `teardown()` on extension instances that were NOT added to a core instance; `core.unload()` handles teardown for initialized extensions
+- **Hook Validation**: The framework validates that all hooks are properly removed; tests will fail if cleanup is incomplete
+- **Resource Isolation**: Each test must be completely isolated - no shared state or leftover hooks
+
+### Configuration Testing Requirements
+- **Static Configuration**: Test initial configuration setup and validation
+- **Dynamic Configuration**: **REQUIRED** - All tests that touch configuration must include post-initialization configuration change tests
+- **onConfigChange Testing**: Components using `onConfigChange` callbacks must be tested for runtime configuration updates
+- **Configuration Validation**: Test both valid and invalid configuration scenarios with proper error handling
+
+```typescript
+// Example dynamic configuration test pattern
+public testDynamicConfig() {
+ // Initial setup with one config
+ let initialConfig = { enableFeature: false };
+ core.initialize(initialConfig, channels);
+
+ // Verify initial behavior
+ Assert.equal(false, component.isFeatureEnabled());
+
+ // Update configuration dynamically
+ core.config.enableFeature = true;
+ // Note: core.onConfigChange() only registers callbacks, it doesn't trigger changes
+
+ // To trigger config change detection, use one of these patterns:
+
+ // Option 1: Using fake timers (synchronous)
+ this.clock.tick(1); // Trigger 1ms timer for config change detection
+
+ // Option 2: Async test without fake timers
+ // return createPromise((resolve) => {
+ // setTimeout(() => {
+ // Assert.equal(true, component.isFeatureEnabled());
+ // resolve();
+ // }, 10);
+ // });
+
+ // Verify behavior changed (when using fake timers)
+ Assert.equal(true, component.isFeatureEnabled());
+}
+```
+
+### Package Organization & Dependencies
+- **Respect Package Boundaries**: Place tests in the package that owns the functionality being tested
+- **Dependency Injection**: Extensions must include dependencies in `config.extensions` array for proper initialization
+- **Cross-Package Coordination**: Understand which package owns which functionality when testing integrated features
+- **Import Resolution**: Use proper module imports and avoid direct file path dependencies
+
+### HTTP API & Network Testing
+- **Use Framework Helpers**: Use `this.hookFetch()` instead of custom fetch mocking implementations
+- **XMLHttpRequest Testing**: Use framework's built-in mechanisms for XHR validation
+- **Header Validation**: Test both presence and absence of headers based on different configuration modes
+- **Network Scenarios**: Test success, failure, timeout, and edge cases consistently
+
+### Async Testing Patterns
+- **IPromise Return**: Use `this.testCase()` and return `IPromise` for asynchronous operations instead of deprecated `testCaseAsync`
+- **Promise Handling**: Handle both resolution and rejection paths in async tests
+- **Timing Control**: Use `this.clock.tick()` when `useFakeTimers: true` for deterministic timing
+- **Cleanup in Async**: Ensure cleanup happens in both success and failure paths of async tests
+
+```typescript
+// Example async test pattern
+this.testCase({
+ name: "Async operation test",
+ test: () => {
+ return createPromise((resolve, reject) => {
+ // Setup async operation
+ someAsyncOperation().then(() => {
+ try {
+ // Assertions
+ Assert.ok(true, "Operation succeeded");
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }).catch(reject);
+ });
+ }
+});
+```
+
+### Unit Testing Best Practices
+- **Comprehensive Coverage**: Test all major code paths including edge cases and error conditions
+- **Mock Browser APIs**: Mock browser APIs consistently using framework-provided mechanisms
+- **Telemetry Validation**: Verify telemetry data structure, content, and proper formatting
+- **State Testing**: Test both empty/null states and populated states for data structures
+
### Browser Testing
-- Cross-browser compatibility testing
-- Performance regression testing
-- Memory leak detection
-- Network failure scenarios
+- **Cross-browser Compatibility**: Test across different browser environments and API availability
+- **Performance Regression**: Monitor test execution time and detect performance regressions
+- **Memory Leak Detection**: Verify proper cleanup and resource management in long-running scenarios
+- **API Graceful Degradation**: Test behavior when browser APIs are unavailable or disabled
### Test Organization
-- Collocate tests with source code in `Tests/` directories
-- Use descriptive test names
-- Group related tests in test suites
-- Mock external dependencies
+- **Collocate Tests**: Place tests in `Tests/` directories within the same package as source code
+- **Descriptive Naming**: Use clear, descriptive test names that explain the scenario being tested
+- **Logical Grouping**: Group related tests in test suites within the same test class
+- **Documentation**: Include comments explaining complex test scenarios and edge cases
+
+### Common Anti-Patterns to Avoid
+- **Skipping Cleanup**: Not calling `unload()` or `teardown()` methods leads to test interference
+- **Custom Implementations**: Implementing custom mocks/helpers instead of using framework-provided tools
+- **Configuration Gaps**: Testing only static configuration without dynamic configuration change scenarios
+- **Hook Pollution**: Leaving hooks active between tests causing false positives/negatives
+- **Incomplete Coverage**: Missing edge cases, error conditions, or state transitions
+- **Deprecated Async**: Using deprecated `testCaseAsync` instead of `testCase` with `IPromise` return
## Configuration & Initialization
diff --git a/AISKU/README.md b/AISKU/README.md
index 14205bb44..034ae081c 100644
--- a/AISKU/README.md
+++ b/AISKU/README.md
@@ -34,6 +34,7 @@ See [Breaking Changes](https://microsoft.github.io/ApplicationInsights-JS/upgrad
| Version | Full Size | Raw Minified | GZip Size
|---------|-----------|--------------|-------------
| [<nightly3>](https://github.com/microsoft/ApplicationInsights-JS/tree/main/AISKU) | [](https://js.monitor.azure.com/nightly/ai.3-nightly3.js.svg)|  | 
+| 3.4.0-beta: | [](https://js.monitor.azure.com/scripts/b/ai.3.4.0-beta.js.svg)|  | 
| 3.3.11: | [](https://js.monitor.azure.com/scripts/b/ai.3.3.11.js.svg)|  | 
| 3.3.10: | [](https://js.monitor.azure.com/scripts/b/ai.3.3.10.js.svg)|  | 
| 3.3.9: | [](https://js.monitor.azure.com/scripts/b/ai.3.3.9.js.svg)|  | 
diff --git a/AISKU/Tests/Manual/README.md b/AISKU/Tests/Manual/README.md
new file mode 100644
index 000000000..090aacdcf
--- /dev/null
+++ b/AISKU/Tests/Manual/README.md
@@ -0,0 +1,262 @@
+# Span API End-to-End (E2E) Tests
+
+This directory contains end-to-end tests for the new Span APIs that send real telemetry to Azure Application Insights (Breeze endpoint) for manual validation in the Azure Portal.
+
+## 📁 Files
+
+- **`SpanE2E.Tests.ts`** - Automated E2E test suite that can be configured to send real telemetry
+- **`span-e2e-manual-test.html`** - Interactive HTML page for manual testing with visual feedback
+
+## 🚀 Quick Start - Manual HTML Testing
+
+The easiest way to test is using the interactive HTML page:
+
+1. **Get your Application Insights credentials**:
+ - Go to [Azure Portal](https://portal.azure.com)
+ - Navigate to your Application Insights resource (or create a new one)
+ - Copy the **Instrumentation Key** or **Connection String** from the Overview page
+
+2. **Open the test page**:
+ ```bash
+ # Option 1: Open directly in browser
+ open AISKU/Tests/Manual/span-e2e-manual-test.html
+
+ # Option 2: Serve via local web server
+ cd AISKU/Tests/Manual
+ python -m http.server 8080
+ # Then open http://localhost:8080/span-e2e-manual-test.html
+ ```
+
+3. **Run tests**:
+ - Paste your Instrumentation Key or Connection String
+ - Click "Initialize SDK"
+ - Run individual tests or click "Run All Tests"
+ - Watch the output log for confirmation
+
+4. **View results in Azure Portal**:
+ - Wait 1-2 minutes for telemetry to arrive
+ - Go to your Application Insights resource
+ - Navigate to **Performance** → **Dependencies** or **Requests**
+ - Use **Search** to find specific test scenarios
+ - Click **"View in End-to-End Transaction"** to see distributed traces
+
+## 🧪 Automated Test Suite
+
+### Configuration
+
+To run the automated test suite with real telemetry:
+
+1. Open [`SpanE2E.Tests.ts`](../Unit/src/SpanE2E.Tests.ts)
+
+2. Update the configuration:
+ ```typescript
+ // Set to true to send real telemetry
+ private static readonly MANUAL_E2E_TEST = true;
+
+ // Replace with your instrumentation key
+ private static readonly _instrumentationKey = "YOUR-IKEY-HERE";
+ ```
+
+3. Run the tests:
+ ```bash
+ # From repository root
+ rush build
+ rush test
+ ```
+
+### Test Scenarios Included
+
+The test suite covers:
+
+#### Basic Span Tests
+- ✅ CLIENT span → RemoteDependency
+- ✅ SERVER span → Request
+- ✅ Failed span → success=false
+
+#### Distributed Trace Tests
+- ✅ Parent-child relationships
+- ✅ 3-level nested hierarchy
+- ✅ Context propagation
+
+#### HTTP Dependency Tests
+- ✅ Various HTTP methods (GET, POST, PUT, DELETE)
+- ✅ Multiple status codes (2xx, 4xx, 5xx)
+- ✅ Full HTTP details (headers, body size, response time)
+
+#### Database Dependency Tests
+- ✅ MySQL, PostgreSQL, MongoDB, Redis, SQL Server
+- ✅ SQL statements and operations
+- ✅ Slow query scenarios
+
+#### Complex Scenarios
+- ✅ E-commerce checkout flow (7 dependencies)
+- ✅ Mixed success and failure operations
+- ✅ Rich custom properties for filtering
+
+## 🔍 What to Look For in the Portal
+
+### Performance Blade
+
+**Dependencies Tab**:
+- Look for CLIENT, PRODUCER, and INTERNAL spans
+- Verify dependency types (Http, mysql, postgresql, redis, etc.)
+- Check duration, target, and result codes
+- Examine custom properties in the details pane
+
+**Requests Tab**:
+- Look for SERVER and CONSUMER spans
+- Verify URLs, methods, and status codes
+- Check success/failure status
+- View response codes and durations
+
+### Search Feature
+
+Filter by custom properties to find specific test runs:
+```
+customDimensions.test.scenario == "ecommerce"
+customDimensions.test.timestamp >= datetime(2025-12-01)
+customDimensions.business.tenant == "manual-test-corp"
+```
+
+### End-to-End Transaction View
+
+1. Click any request or dependency
+2. Click **"View in End-to-End Transaction"**
+3. See the complete distributed trace:
+ - Timeline showing span durations
+ - Parent-child relationships
+ - All related dependencies
+ - Custom properties at each level
+
+### Transaction Timeline
+
+Look for:
+- ✅ Correct parent-child relationships (indentation)
+- ✅ Proper span nesting (visual hierarchy)
+- ✅ Accurate duration calculations
+- ✅ Operation IDs matching across spans
+- ✅ Custom dimensions preserved throughout
+
+## 📊 Expected Results
+
+### Test: Basic CLIENT Span
+- **Portal Location**: Performance → Dependencies
+- **Dependency Type**: "Dependency" or "Http"
+- **Custom Properties**: test.scenario, test.timestamp
+
+### Test: Parent-Child Trace
+- **Portal Location**: End-to-End Transaction view
+- **Expected**: 1 Request + 2 Dependencies
+- **Relationship**: Both children reference same parent operation.id
+
+### Test: E-commerce Checkout
+- **Portal Location**: End-to-End Transaction view
+- **Expected**: 1 Request + 7 Dependencies
+- **Types**: Http (inventory, payment, email), Database (create order), Redis (cache)
+- **Duration**: Parent spans entire operation
+
+### Test: Rich Custom Properties
+- **Portal Location**: Search → Custom dimensions filter
+- **Expected Properties**:
+ - business.tenant
+ - user.subscription
+ - feature.* flags
+ - performance.* metrics
+
+## 🐛 Troubleshooting
+
+### Telemetry not appearing in portal
+
+1. **Wait longer**: Initial ingestion can take 1-3 minutes
+2. **Check time filter**: Ensure portal is showing last 30 minutes
+3. **Verify iKey**: Confirm instrumentation key is correct
+4. **Check browser console**: Look for SDK errors
+5. **Flush telemetry**: Call `appInsights.flush()` in tests
+
+### SDK initialization fails
+
+1. **Valid credentials**: Verify instrumentation key format
+2. **CORS issues**: Ensure application is running on http/https (not file://)
+3. **Browser compatibility**: Use modern browser (Chrome, Edge, Firefox)
+
+### Missing custom properties
+
+1. **Property name limits**: Check for truncation (8192 char limit)
+2. **Reserved names**: Some property names are filtered (http.*, db.*, microsoft.*)
+3. **Type preservation**: Ensure values are correct types (string, number, boolean)
+
+## 📝 Adding New Test Scenarios
+
+To add a new E2E test scenario:
+
+1. **In SpanE2E.Tests.ts**:
+ ```typescript
+ this.testCase({
+ name: "E2E: Your new scenario",
+ test: () => {
+ const span = this._ai.startSpan("E2E-YourScenario", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.scenario": "your-scenario",
+ "custom.property": "value"
+ }
+ });
+
+ if (span) {
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+
+ this._ai.flush();
+ Assert.ok(span, "Span created");
+ }
+ });
+ ```
+
+2. **In span-e2e-manual-test.html**:
+ ```javascript
+ function testYourScenario() {
+ if (!appInsights) return;
+
+ const span = appInsights.startSpan('E2E-Manual-YourScenario', {
+ kind: 1, // CLIENT
+ attributes: {
+ 'test.scenario': 'your-scenario',
+ 'custom.property': 'value'
+ }
+ });
+
+ if (span) {
+ span.setStatus({ code: 1 });
+ span.end();
+ log('✅ Your scenario sent', 'success');
+ }
+ appInsights.flush();
+ }
+ ```
+
+## 🎯 Best Practices
+
+1. **Use descriptive names**: Prefix test spans with "E2E-" or "Manual-"
+2. **Include timestamps**: Add test.timestamp for filtering
+3. **Add scenario tags**: Use test.scenario for grouping
+4. **Flush after tests**: Always call `flush()` to send immediately
+5. **Wait before checking**: Give telemetry 1-2 minutes to arrive
+6. **Use unique identifiers**: Help distinguish between test runs
+7. **Clean up regularly**: Archive or delete old test data
+
+## 🔗 Resources
+
+- [Application Insights Overview](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview)
+- [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/trace/api/)
+- [Azure Portal](https://portal.azure.com)
+- [Application Insights SDK Documentation](../../../README.md)
+
+## 🤝 Contributing
+
+When adding new E2E tests:
+1. Follow existing naming conventions (E2E-* prefix)
+2. Include relevant custom properties
+3. Document expected portal behavior
+4. Update this README with new scenarios
+5. Test manually before committing
diff --git a/AISKU/Tests/Manual/span-e2e-manual-test.html b/AISKU/Tests/Manual/span-e2e-manual-test.html
new file mode 100644
index 000000000..00172a525
--- /dev/null
+++ b/AISKU/Tests/Manual/span-e2e-manual-test.html
@@ -0,0 +1,818 @@
+
+
+
+
+
+ Application Insights - Span API Manual E2E Test
+
+
+
+
+
+
+
⚙️ Configuration
+
+
+
+
+ Get this from: Azure Portal → Your Application Insights resource → Overview → Instrumentation Key
+
+
+
+
+
+
+
+
🧪 Test Scenarios
+
+
Basic Tests
+
+
+
+
+
Distributed Trace Tests
+
+
+
+
Dependency Tests
+
+
+
+
Complex Scenarios
+
+
+
+
+
Batch Actions
+
+
+
+
+
+
Utilities
+
+
+
+
+
+
+
+
📝 Output Log
+
+ Ready to start testing. Initialize the SDK first.
+
+
+
+
+
🔗 View Results
+
After running tests, wait 1-2 minutes for telemetry to appear in the portal.
+
Open Azure Portal
+
+
+
💡 Tips for viewing in portal:
+
+ - Go to Application Insights → Performance to see requests and dependencies
+ - Use the Search feature to find specific operations by custom properties
+ - Click "View in End-to-End Transaction" to see distributed traces
+ - Use the Timeline view to see span relationships
+ - Filter by operation name like "E2E-CheckoutRequest"
+ - Check custom dimensions for test.scenario and test.timestamp
+
+
+
+
+
+
+
+
+
+
diff --git a/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts b/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts
index 19aa62857..023de9783 100644
--- a/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts
+++ b/AISKU/Tests/Perf/src/AISKUPerf.Tests.ts
@@ -1,6 +1,6 @@
import { AITestClass, Assert } from "@microsoft/ai-test-framework";
import { AppInsightsInitPerfTestClass } from "./AISKUPerf";
-import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-common";
+import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-core-js";
import { createTimeoutPromise, doAwait } from "@nevware21/ts-async";
function isNullOrUndefined(value: any): boolean {
@@ -153,8 +153,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() {
diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts
index 2a7782040..e48d81793 100644
--- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts
+++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts
@@ -2,7 +2,7 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework";
import { dumpObj } from '@nevware21/ts-utils';
import { createPromise, doAwait, IPromise } from '@nevware21/ts-async';
import { strUndefined } from "@microsoft/applicationinsights-core-js";
-import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-common";
+import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-core-js";
import * as pako from "pako";
import { Snippet } from "../../../src/Snippet";
@@ -46,18 +46,18 @@ function _loadPackageJson(cb:(isNightly: boolean, packageJson: any) => IPromise<
}
function _checkSize(checkType: string, maxSize: number, size: number, isNightly: boolean): void {
- if (isNightly) {
+ if (isNightly) {
maxSize += .5;
}
Assert.ok(size <= maxSize, `exceed ${maxSize} KB, current ${checkType} size is: ${size} KB`);
-}
+}
export class AISKUSizeCheck extends AITestClass {
- private readonly MAX_RAW_SIZE = 150;
- private readonly MAX_BUNDLE_SIZE = 150;
- private readonly MAX_RAW_DEFLATE_SIZE = 60;
- private readonly MAX_BUNDLE_DEFLATE_SIZE = 60;
+ private readonly MAX_RAW_SIZE = 174;
+ private readonly MAX_BUNDLE_SIZE = 174;
+ private readonly MAX_RAW_DEFLATE_SIZE = 70;
+ private readonly MAX_BUNDLE_DEFLATE_SIZE = 70;
private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js";
// Automatically updated by version scripts
private readonly currentVer = "3.3.11";
@@ -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() {
diff --git a/AISKU/Tests/Unit/src/CdnPackaging.tests.ts b/AISKU/Tests/Unit/src/CdnPackaging.tests.ts
index 5d84b058b..2aa3fd8e5 100644
--- a/AISKU/Tests/Unit/src/CdnPackaging.tests.ts
+++ b/AISKU/Tests/Unit/src/CdnPackaging.tests.ts
@@ -2,7 +2,7 @@ import { AITestClass, Assert } from "@microsoft/ai-test-framework";
import {
AnalyticsPluginIdentifier, BreezeChannelIdentifier, DEFAULT_BREEZE_ENDPOINT, DisabledPropertyName,
DistributedTracingModes, PropertiesPluginIdentifier, RequestHeaders, SeverityLevel
-} from "@microsoft/applicationinsights-common";
+} from "@microsoft/applicationinsights-core-js";
import { dumpObj, LoggingSeverity, objForEachKey, objKeys, strUndefined } from "@microsoft/applicationinsights-core-js";
import { Snippet } from "../../../src/Snippet";
diff --git a/AISKU/Tests/Unit/src/CdnThrottle.tests.ts b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts
index 2d3142132..f0e0f44c6 100644
--- a/AISKU/Tests/Unit/src/CdnThrottle.tests.ts
+++ b/AISKU/Tests/Unit/src/CdnThrottle.tests.ts
@@ -1,6 +1,6 @@
import { ApplicationInsights, ApplicationInsightsContainer, IApplicationInsights, IConfig, IConfiguration, LoggingSeverity, Snippet, _eInternalMessageId } from '../../../src/applicationinsights-web'
import { AITestClass, Assert, IFetchArgs, PollingAssert} from '@microsoft/ai-test-framework';
-import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig } from '@microsoft/applicationinsights-common';
+import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig } from '@microsoft/applicationinsights-core-js';
import { SinonSpy } from 'sinon';
import { AppInsightsSku } from '../../../src/AISku';
import { createSnippetV5 } from './testSnippetV5';
diff --git a/AISKU/Tests/Unit/src/IAnalyticsConfig.Tests.ts b/AISKU/Tests/Unit/src/IAnalyticsConfig.Tests.ts
index d9bd8c6f5..26d123f2a 100644
--- a/AISKU/Tests/Unit/src/IAnalyticsConfig.Tests.ts
+++ b/AISKU/Tests/Unit/src/IAnalyticsConfig.Tests.ts
@@ -1,6 +1,6 @@
import { ApplicationInsights, IAnalyticsConfig, IAppInsights, IConfig, ApplicationAnalytics } from "../../../src/applicationinsights-web";
import { AITestClass, Assert } from "@microsoft/ai-test-framework";
-import { AnalyticsPluginIdentifier, utlRemoveSessionStorage } from "@microsoft/applicationinsights-common";
+import { AnalyticsPluginIdentifier, utlRemoveSessionStorage } from "@microsoft/applicationinsights-core-js";
import { AppInsightsCore, IConfiguration, isFunction, onConfigChange } from "@microsoft/applicationinsights-core-js";
import { Sender } from "@microsoft/applicationinsights-channel-js";
diff --git a/AISKU/Tests/Unit/src/NonRecordingSpan.Tests.ts b/AISKU/Tests/Unit/src/NonRecordingSpan.Tests.ts
new file mode 100644
index 000000000..12597ed63
--- /dev/null
+++ b/AISKU/Tests/Unit/src/NonRecordingSpan.Tests.ts
@@ -0,0 +1,773 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import { eOTelSpanKind, eOTelSpanStatusCode, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+
+/**
+ * Comprehensive tests for non-recording span behavior
+ *
+ * Non-recording spans are used for:
+ * - Context propagation without telemetry overhead
+ * - Testing and debugging scenarios
+ * - Wrapping external span contexts
+ * - Performance-sensitive scenarios
+ */
+export class NonRecordingSpanTests extends AITestClass {
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${NonRecordingSpanTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "NonRecordingSpanTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: NonRecordingSpanTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+ } catch (e) {
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addBasicNonRecordingTests();
+ this.addAttributeOperationTests();
+ this.addStatusAndNameTests();
+ this.addSpanKindTests();
+ this.addHierarchyTests();
+ this.addTelemetryGenerationTests();
+ this.addPerformanceTests();
+ this.addEdgeCaseTests();
+ }
+
+ private addBasicNonRecordingTests(): void {
+ this.testCase({
+ name: "NonRecording: span created with recording:false is not recording",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("non-recording-basic", { recording: false });
+
+ // Assert
+ Assert.ok(span, "Span should be created");
+ Assert.ok(!span.isRecording(), "Span should not be recording");
+ Assert.equal(span.name, "non-recording-basic", "Span name should be set");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: default recording:true creates recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("recording-default");
+
+ // Assert
+ Assert.ok(span, "Span should be created");
+ Assert.ok(span.isRecording(), "Span should be recording by default");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: explicit recording:true creates recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("recording-explicit", { recording: true });
+
+ // Assert
+ Assert.ok(span, "Span should be created");
+ Assert.ok(span.isRecording(), "Span should be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: isRecording() returns false throughout lifecycle",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-lifecycle", { recording: false });
+
+ // Act & Assert - Before operations
+ Assert.ok(!span.isRecording(), "Should not be recording initially");
+
+ // Perform operations
+ span!.setAttribute("key", "value");
+ Assert.ok(!span.isRecording(), "Should not be recording after setAttribute");
+
+ span!.setStatus({ code: eOTelSpanStatusCode.OK });
+ Assert.ok(!span.isRecording(), "Should not be recording after setStatus");
+
+ span!.updateName("new-name");
+ Assert.ok(!span.isRecording(), "Should not be recording after updateName");
+
+ span!.end();
+ Assert.ok(!span.isRecording(), "Should not be recording after end");
+ }
+ });
+ }
+
+ private addAttributeOperationTests(): void {
+ this.testCase({
+ name: "NonRecording: setAttribute does not store attributes",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-attrs", { recording: false });
+
+ // Act
+ span!.setAttribute("key1", "value1");
+ span!.setAttribute("key2", 123);
+ span!.setAttribute("key3", true);
+
+ // Assert
+ const attrs = span!.attributes;
+ Assert.ok(attrs, "Attributes object should exist");
+ // Non-recording spans don't store attributes
+ Assert.equal(Object.keys(attrs).length, 0, "No attributes should be stored");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: setAttributes does not store attributes",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-set-attrs", { recording: false });
+
+ // Act
+ span!.setAttributes({
+ "attr1": "value1",
+ "attr2": 456,
+ "attr3": false,
+ "attr4": [1, 2, 3]
+ });
+
+ // Assert
+ const attrs = span!.attributes;
+ Assert.equal(Object.keys(attrs).length, 0, "No attributes should be stored");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: setAttribute returns span for chaining",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-chain", { recording: false });
+
+ // Act
+ const result = span!.setAttribute("key", "value");
+
+ // Assert
+ Assert.equal(result, span, "setAttribute should return the span for chaining");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: setAttributes returns span for chaining",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-chain-multi", { recording: false });
+
+ // Act
+ const result = span!.setAttributes({ "key1": "value1", "key2": "value2" });
+
+ // Assert
+ Assert.equal(result, span, "setAttributes should return the span for chaining");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: multiple setAttribute calls increment dropped count",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-dropped", { recording: false });
+
+ // Act
+ span!.setAttribute("key1", "value1");
+ span!.setAttribute("key2", "value2");
+ span!.setAttribute("key3", "value3");
+ span!.setAttributes({ "key4": "value4", "key5": "value5" });
+
+ // Assert
+ const droppedCount = span!.droppedAttributesCount;
+ Assert.ok(droppedCount >= 5, `At least 5 attributes should be dropped, got ${droppedCount}`);
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: setAttribute after end() increments dropped count",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-after-end", { recording: false });
+ span!.end();
+
+ // Act
+ span!.setAttribute("late-key", "late-value");
+
+ // Assert - Should not throw, just increment dropped count
+ Assert.ok(span.ended, "Span should be ended");
+ Assert.ok(span.droppedAttributesCount > 0, "Dropped attribute count should be incremented");
+ }
+ });
+ }
+
+ private addStatusAndNameTests(): void {
+ this.testCase({
+ name: "NonRecording: setStatus changes status even when not recording",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-status", { recording: false });
+
+ // Act
+ span!.setStatus({ code: eOTelSpanStatusCode.ERROR, message: "Test error" });
+
+ // Assert
+ Assert.equal(span.status.code, eOTelSpanStatusCode.ERROR, "Status code should be set");
+ Assert.equal(span.status.message, "Test error", "Status message should be set");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: setStatus returns span for chaining",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-status-chain", { recording: false });
+
+ // Act
+ const result = span!.setStatus({ code: eOTelSpanStatusCode.OK });
+
+ // Assert
+ Assert.equal(result, span, "setStatus should return the span for chaining");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: updateName changes name even when not recording",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("original-name", { recording: false });
+
+ // Act
+ span!.updateName("updated-name");
+
+ // Assert
+ Assert.equal(span.name, "updated-name", "Name should be updated");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: updateName returns span for chaining",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("chain-name", { recording: false });
+
+ // Act
+ const result = span!.updateName("new-chain-name");
+
+ // Assert
+ Assert.equal(result, span, "updateName should return the span for chaining");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: chained operations work correctly",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("chaining-test", { recording: false });
+
+ // Act
+ const result = span
+ .setAttribute("key1", "value1")
+ .setAttributes({ "key2": "value2" })
+ .setStatus({ code: eOTelSpanStatusCode.OK })
+ .updateName("chained-name");
+
+ // Assert
+ Assert.equal(result, span, "All operations should return the span");
+ Assert.equal(span.name, "chained-name", "Name should be updated");
+ Assert.equal(span.status.code, eOTelSpanStatusCode.OK, "Status should be set");
+ }
+ });
+ }
+
+ private addSpanKindTests(): void {
+ this.testCase({
+ name: "NonRecording: CLIENT kind non-recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("client-non-recording", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: false
+ });
+
+ // Assert
+ Assert.ok(span, "Span should be created");
+ Assert.equal(span.kind, eOTelSpanKind.CLIENT, "Kind should be CLIENT");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: SERVER kind non-recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("server-non-recording", {
+ kind: eOTelSpanKind.SERVER,
+ recording: false
+ });
+
+ // Assert
+ Assert.equal(span.kind, eOTelSpanKind.SERVER, "Kind should be SERVER");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: INTERNAL kind non-recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("internal-non-recording", {
+ kind: eOTelSpanKind.INTERNAL,
+ recording: false
+ });
+
+ // Assert
+ Assert.equal(span.kind, eOTelSpanKind.INTERNAL, "Kind should be INTERNAL");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: PRODUCER kind non-recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("producer-non-recording", {
+ kind: eOTelSpanKind.PRODUCER,
+ recording: false
+ });
+
+ // Assert
+ Assert.equal(span.kind, eOTelSpanKind.PRODUCER, "Kind should be PRODUCER");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: CONSUMER kind non-recording span",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("consumer-non-recording", {
+ kind: eOTelSpanKind.CONSUMER,
+ recording: false
+ });
+
+ // Assert
+ Assert.equal(span.kind, eOTelSpanKind.CONSUMER, "Kind should be CONSUMER");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+ }
+
+ private addHierarchyTests(): void {
+ this.testCase({
+ name: "NonRecording: parent recording, child non-recording",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("recording-parent", {
+ kind: eOTelSpanKind.SERVER,
+ recording: true
+ });
+ const parentContext = parentSpan!.spanContext();
+
+ // Act
+ const childSpan = this._ai.startSpan("non-recording-child", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: false
+ }, parentContext);
+
+ // Assert
+ Assert.ok(parentSpan!.isRecording(), "Parent should be recording");
+ Assert.ok(!childSpan.isRecording(), "Child should not be recording");
+ Assert.equal(childSpan!.spanContext().traceId, parentContext.traceId,
+ "Child should share parent's trace ID");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: parent non-recording, child recording",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("non-recording-parent", {
+ kind: eOTelSpanKind.SERVER,
+ recording: false
+ });
+ const parentContext = parentSpan!.spanContext();
+
+ // Act
+ const childSpan = this._ai.startSpan("recording-child", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: true
+ }, parentContext);
+
+ // Assert
+ Assert.ok(!parentSpan.isRecording(), "Parent should not be recording");
+ Assert.ok(childSpan!.isRecording(), "Child should be recording");
+ Assert.equal(childSpan!.spanContext().traceId, parentContext.traceId,
+ "Child should share parent's trace ID");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: both parent and child non-recording",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("non-recording-parent-2", {
+ recording: false
+ });
+ const parentContext = parentSpan!.spanContext();
+
+ // Act
+ const childSpan = this._ai.startSpan("non-recording-child-2", {
+ recording: false
+ }, parentContext);
+
+ // Assert
+ Assert.ok(!parentSpan.isRecording(), "Parent should not be recording");
+ Assert.ok(!childSpan.isRecording(), "Child should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: multi-level hierarchy with mixed recording",
+ test: () => {
+ // Arrange
+ const level1 = this._ai.startSpan("level1-recording", { recording: true });
+ const level1Context = level1!.spanContext();
+
+ const level2 = this._ai.startSpan("level2-non-recording", {
+ recording: false
+ }, level1Context);
+ const level2Context = level2!.spanContext();
+
+ const level3 = this._ai.startSpan("level3-recording", {
+ recording: true
+ }, level2Context);
+
+ // Assert
+ Assert.ok(level1!.isRecording(), "Level 1 should be recording");
+ Assert.ok(!level2.isRecording(), "Level 2 should not be recording");
+ Assert.ok(level3!.isRecording(), "Level 3 should be recording");
+
+ // All should share the same trace ID
+ Assert.equal(level2!.spanContext().traceId, level1Context.traceId,
+ "Level 2 should share trace ID");
+ Assert.equal(level3!.spanContext().traceId, level1Context.traceId,
+ "Level 3 should share trace ID");
+ }
+ });
+ }
+
+ private addTelemetryGenerationTests(): void {
+ this.testCase({
+ name: "NonRecording: no telemetry generated on end()",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("non-recording-no-telemetry", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: false
+ });
+ span!.setAttribute("should-not-appear", "in-telemetry");
+ span!.setStatus({ code: eOTelSpanStatusCode.OK });
+ span!.end();
+
+ // Assert
+ const telemetryItem = this._trackCalls.find(
+ item => item.baseData?.name === "non-recording-no-telemetry"
+ );
+ Assert.ok(!telemetryItem, "Non-recording span should not generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: recording span generates telemetry, non-recording does not",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const recordingSpan = this._ai.startSpan("recording-generates", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: true
+ });
+ recordingSpan.end();
+
+ const nonRecordingSpan = this._ai.startSpan("non-recording-silent", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: false
+ });
+ nonRecordingSpan.end();
+
+ // Assert
+ const recordingTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "recording-generates"
+ );
+ const nonRecordingTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "non-recording-silent"
+ );
+
+ Assert.ok(recordingTelemetry, "Recording span should generate telemetry");
+ Assert.ok(!nonRecordingTelemetry, "Non-recording span should not generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: parent recording generates telemetry, child non-recording does not",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const parent = this._ai.startSpan("parent-with-telemetry", {
+ kind: eOTelSpanKind.SERVER,
+ recording: true
+ });
+ const parentContext = parent.spanContext();
+
+ const child = this._ai.startSpan("child-without-telemetry", {
+ kind: eOTelSpanKind.CLIENT,
+ recording: false
+ }, parentContext);
+
+ child.end();
+ parent.end();
+
+ // Assert
+ const parentTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "parent-with-telemetry"
+ );
+ const childTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "child-without-telemetry"
+ );
+
+ Assert.ok(parentTelemetry, "Parent recording span should generate telemetry");
+ Assert.ok(!childTelemetry, "Child non-recording span should not generate telemetry");
+ }
+ });
+ }
+
+ private addPerformanceTests(): void {
+ this.testCase({
+ name: "NonRecording: multiple non-recording spans minimal overhead",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spanCount = 100;
+
+ // Act
+ const startTime = Date.now();
+ for (let i = 0; i < spanCount; i++) {
+ const span = this._ai.startSpan(`non-recording-perf-${i}`, {
+ recording: false
+ });
+ span!.setAttribute("iteration", i);
+ span!.setStatus({ code: eOTelSpanStatusCode.OK });
+ span!.end();
+ }
+ const elapsed = Date.now() - startTime;
+
+ // Assert
+ Assert.ok(elapsed < 1000, `Creating ${spanCount} non-recording spans should be fast, took ${elapsed}ms`);
+ Assert.equal(this._trackCalls.length, 0, "No telemetry should be generated for non-recording spans");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: attribute operations are fast on non-recording spans",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("perf-attrs", { recording: false });
+ const attrCount = 1000;
+
+ // Act
+ const startTime = Date.now();
+ for (let i = 0; i < attrCount; i++) {
+ span!.setAttribute(`key${i}`, `value${i}`);
+ }
+ const elapsed = Date.now() - startTime;
+
+ // Assert
+ Assert.ok(elapsed < 500, `Setting ${attrCount} attributes should be fast, took ${elapsed}ms`);
+ Assert.equal(Object.keys(span.attributes).length, 0, "Attributes should not be stored");
+ }
+ });
+ }
+
+ private addEdgeCaseTests(): void {
+ this.testCase({
+ name: "NonRecording: end() can be called multiple times safely",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("multi-end", { recording: false });
+
+ // Act & Assert - Should not throw
+ span!.end();
+ Assert.ok(span.ended, "Span should be ended");
+
+ span!.end();
+ Assert.ok(span.ended, "Span should still be ended");
+
+ span!.end();
+ Assert.ok(span.ended, "Span should still be ended");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: operations after end() do not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ops-after-end", { recording: false });
+ span!.end();
+
+ // Act & Assert - Should not throw
+ span!.setAttribute("late-attr", "value");
+ span!.setAttributes({ "late-attrs": "values" });
+ span!.setStatus({ code: eOTelSpanStatusCode.ERROR });
+ span!.updateName("late-name");
+
+ Assert.ok(span.ended, "Span should remain ended");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: null and undefined attribute values handled",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-attrs", { recording: false });
+
+ // Act & Assert - Should not throw
+ span!.setAttribute("null-value", null as any);
+ span!.setAttribute("undefined-value", undefined as any);
+ span!.setAttributes({
+ "null-in-set": null as any,
+ "undefined-in-set": undefined as any
+ });
+
+ Assert.ok(!span.isRecording(), "Span should still be non-recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: empty string name allowed",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("", { recording: false });
+
+ // Assert
+ Assert.ok(span, "Span with empty name should be created");
+ Assert.equal(span.name, "", "Name should be empty string");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: special characters in span name",
+ test: () => {
+ // Arrange
+ const specialNames = [
+ "span/with/slashes",
+ "span:with:colons",
+ "span-with-dashes",
+ "span.with.dots",
+ "span with spaces",
+ "span\twith\ttabs",
+ "span(with)parens",
+ "span[with]brackets",
+ "span{with}braces"
+ ];
+
+ // Act & Assert
+ specialNames.forEach(name => {
+ const span = this._ai.startSpan(name, { recording: false });
+ Assert.ok(span, `Span with name '${name}' should be created`);
+ Assert.equal(span.name, name, "Name should be preserved");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ });
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: very long span name handled",
+ test: () => {
+ // Arrange
+ const longName = "a".repeat(10000);
+
+ // Act
+ const span = this._ai.startSpan(longName, { recording: false });
+
+ // Assert
+ Assert.ok(span, "Span with very long name should be created");
+ Assert.ok(!span.isRecording(), "Should not be recording");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: spanContext() returns valid context",
+ test: () => {
+ // Act
+ const span = this._ai.startSpan("context-check", { recording: false });
+ const context = span!.spanContext();
+
+ // Assert
+ Assert.ok(context, "Context should exist");
+ Assert.ok(context.traceId, "Trace ID should exist");
+ Assert.ok(context.spanId, "Span ID should exist");
+ Assert.ok(context.traceId.length === 32, "Trace ID should be 32 characters");
+ Assert.ok(context.spanId.length === 16, "Span ID should be 16 characters");
+ }
+ });
+
+ this.testCase({
+ name: "NonRecording: status object immutability",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("status-immutable", { recording: false });
+ span!.setStatus({ code: eOTelSpanStatusCode.OK, message: "Initial" });
+
+ // Act
+ const status1 = span!.status;
+ span!.setStatus({ code: eOTelSpanStatusCode.ERROR, message: "Changed" });
+ const status2 = span!.status;
+
+ // Assert
+ Assert.equal(status1.code, eOTelSpanStatusCode.OK, "First status should be OK");
+ Assert.equal(status2.code, eOTelSpanStatusCode.ERROR, "Second status should be ERROR");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/OTelInit.Tests.ts b/AISKU/Tests/Unit/src/OTelInit.Tests.ts
new file mode 100644
index 000000000..7b311d8e7
--- /dev/null
+++ b/AISKU/Tests/Unit/src/OTelInit.Tests.ts
@@ -0,0 +1,112 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import { eOTelSpanKind, eOTelSpanStatusCode, isTracingSuppressed, ITelemetryItem, unsuppressTracing } from "@microsoft/applicationinsights-core-js";
+import { objIs, setBypassLazyCache } from "@nevware21/ts-utils";
+import { AnalyticsPluginIdentifier, PropertiesPluginIdentifier } from "@microsoft/applicationinsights-core-js";
+
+/**
+ * Integration Tests for Span APIs with Properties Plugin and Analytics Plugin
+ *
+ * Tests verify that span telemetry correctly integrates with:
+ * - PropertiesPlugin: session, user, device, application context
+ * - AnalyticsPlugin: telemetry creation, dependency tracking, page views
+ * - Telemetry Initializers: custom property injection
+ * - SDK configuration: sampling, disabled tracking, etc.
+ */
+export class OTelInitTests extends AITestClass {
+ private _ai!: ApplicationInsights;
+
+ constructor(testName?: string) {
+ super(testName || "OTelInitTests");
+ }
+
+ public testInitialize() {
+ try {
+ setBypassLazyCache(true);
+ this.useFakeServer = true;
+
+ this._ai = new ApplicationInsights({
+ config: {
+ instrumentationKey: "test-ikey-123",
+ disableInstrumentationKeyValidation: true,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ disableFetchTracking: false,
+ enableAutoRouteTracking: false,
+ disableExceptionTracking: false,
+ maxBatchInterval: 100,
+ enableDebug: false,
+ extensionConfig: {
+ ["AppInsightsPropertiesPlugin"]: {
+ accountId: "test-account-id"
+ }
+ },
+ traceCfg: {
+ coreTrace: 1
+ } as any
+ }
+ });
+
+ this._ai.loadAppInsights();
+ } catch (e) {
+ Assert.ok(false, "Failed to initialize tests: " + e);
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ setBypassLazyCache(false);
+ }
+
+ public registerTests() {
+ this.testCase({
+ name: "OTelInitTests",
+ test: () => {
+ Assert.ok(this._ai, "ApplicationInsights instance should be initialized");
+ Assert.ok(this._ai.getPlugin(PropertiesPluginIdentifier), "PropertiesPlugin should be loaded");
+ Assert.ok(this._ai.getPlugin(AnalyticsPluginIdentifier), "AnalyticsPlugin should be loaded");
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Tracing should not be suppressed by default");
+ }
+ });
+
+ this.testCase({
+ name: "Validate OTelApi",
+ test: () => {
+ const otelApi = this._ai.otelApi;
+ Assert.ok(otelApi, "OTel API should be available");
+ Assert.ok(otelApi.cfg, "OTel configuration should be available");
+ Assert.ok(objIs(this._ai, otelApi.host), "OTel API host should be the same as the SKU instance");
+ Assert.ok(objIs(otelApi.cfg.traceCfg, this._ai.core.config.traceCfg), "OTel trace configuration should be the same as the SDK config");
+ Assert.ok(objIs(otelApi.host.config, this._ai.config), "OTel API config should be the same as the SDK config");
+ Assert.ok(objIs(otelApi.host.config, this._ai.core.config), "OTel API config should be the same as the SDK core config");
+ Assert.ok(objIs(this._ai.config, this._ai.core.config), "SDK config should be the same as the SDK core config");
+ }
+ });
+
+ this.testCase({
+ name: "Validate Trace suppression",
+ test: () => {
+ const otelApi = this._ai.otelApi;
+ Assert.ok(otelApi, "OTel API should be available");
+ Assert.equal(false, isTracingSuppressed(this._ai.core), "Tracing should not be suppressed by default");
+ Assert.equal(false, otelApi.cfg.traceCfg?.suppressTracing, "supressTracing should be false by default");
+ Assert.equal(false, this._ai.core.config.traceCfg.suppressTracing, "suppressTracing should be false by default");
+
+ this._ai.core.config.traceCfg.suppressTracing = true;
+ Assert.equal(true, isTracingSuppressed(this._ai.core), "Tracing should be suppressed when suppressTracing is set to true");
+ Assert.equal(true, otelApi.cfg.traceCfg?.suppressTracing, "supressTracing should be true when suppressTracing is set to true");
+ Assert.equal(true, this._ai.core.config.traceCfg.suppressTracing, "suppressTracing should be true when suppressTracing is set to true");
+
+ unsuppressTracing(this._ai.core);
+ Assert.equal(false, isTracingSuppressed(this._ai.core), "Tracing should not be suppressed after unsuppressTracing");
+ Assert.equal(false, this._ai.core.config.traceCfg.suppressTracing, "suppressTracing should be false by default");
+ Assert.equal(false, otelApi.cfg.traceCfg?.suppressTracing, "supressTracing should be false after unsuppressTracing");
+ }
+ });
+
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts b/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts
index 22c6c85f5..2812f78d9 100644
--- a/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts
+++ b/AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts
@@ -10,10 +10,10 @@ import { BaseTelemetryPlugin, IProcessTelemetryContext, isNotNullOrUndefined, IT
import {
BreezeChannelIdentifier, ContextTagKeys, DistributedTracingModes, IConfig, IDependencyTelemetry, RequestHeaders,
utlRemoveSessionStorage, utlSetSessionStorage
-} from "@microsoft/applicationinsights-common";
+} from "@microsoft/applicationinsights-core-js";
import { getGlobal } from "@microsoft/applicationinsights-shims";
-import { TelemetryContext } from "@microsoft/applicationinsights-properties-js";
-import { dumpObj, isPromiseLike, objHasOwnProperty } from "@nevware21/ts-utils";
+import { IPropTelemetryContext } from "@microsoft/applicationinsights-properties-js";
+import { dumpObj, isPromiseLike, objHasOwnProperty, strSubstring } from "@nevware21/ts-utils";
import { AppInsightsSku } from "../../../src/AISku";
const TestInstrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
@@ -678,7 +678,7 @@ export class SnippetInitializationTests extends AITestClass {
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
const xhr = new XMLHttpRequest();
- xhr.open('GET', 'https://httpbin.org/status/200');
+ xhr.open('GET', 'http://localhost:9001/README.md');
xhr.send();
Assert.ok(true);
}
@@ -694,15 +694,15 @@ export class SnippetInitializationTests extends AITestClass {
steps: [
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- fetch('https://httpbin.org/status/200', { method: 'GET', headers: { 'header': 'value'} });
+ fetch('http://localhost:9001/README.md', { method: 'GET', headers: { 'header': 'value'} });
Assert.ok(true, "fetch monitoring is instrumented");
},
() => {
- fetch('https://httpbin.org/status/200', { method: 'GET' });
+ fetch('http://localhost:9001/README.md', { method: 'GET' });
Assert.ok(true, "fetch monitoring is instrumented");
},
() => {
- fetch('https://httpbin.org/status/200');
+ fetch('http://localhost:9001/README.md');
Assert.ok(true, "fetch monitoring is instrumented");
}
]
@@ -733,6 +733,7 @@ export class SnippetInitializationTests extends AITestClass {
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.requestIdHeader], "Request-Id header");
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.requestContextHeader], "Request-Context header");
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.traceParentHeader], "traceparent");
+ Assert.ok(!baseData.properties.requestHeaders[RequestHeaders.traceStateHeader], "traceState should not be present in outbound event");
const id: string = baseData.id;
const regex = id.match(/\|.{32}\..{16}\./g);
Assert.ok(id.length > 0);
@@ -865,7 +866,7 @@ export class SnippetInitializationTests extends AITestClass {
steps: [
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- const context = (theSnippet.context) as TelemetryContext;
+ const context = (theSnippet.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10001');
theSnippet.trackTrace({ message: 'authUserContext test' });
}
@@ -897,7 +898,7 @@ export class SnippetInitializationTests extends AITestClass {
steps: [
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- const context = (theSnippet.context) as TelemetryContext;
+ const context = (theSnippet.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10001', 'account123');
theSnippet.trackTrace({ message: 'authUserContext test' });
}
@@ -928,7 +929,7 @@ export class SnippetInitializationTests extends AITestClass {
steps: [
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- const context = (theSnippet.context) as TelemetryContext;
+ const context = (theSnippet.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext("\u0428", "\u0429");
theSnippet.trackTrace({ message: 'authUserContext test' });
}
@@ -959,7 +960,7 @@ export class SnippetInitializationTests extends AITestClass {
steps: [
() => {
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- const context = (theSnippet.context) as TelemetryContext;
+ const context = (theSnippet.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10002', 'account567');
context.user.clearAuthenticatedUserContext();
theSnippet.trackTrace({ message: 'authUserContext test' });
@@ -991,7 +992,7 @@ export class SnippetInitializationTests extends AITestClass {
test: () => {
// Setup
let theSnippet = this._initializeSnippet(snippetCreator(getSnippetConfig(this.sessionPrefix)));
- const context = (theSnippet.context) as TelemetryContext;
+ const context = (theSnippet.context) as IPropTelemetryContext;
const authSpy: SinonSpy = this.sandbox.spy(context.user, 'setAuthenticatedUserContext');
let cookieMgr = theSnippet.getCookieMgr();
const cookieSpy: SinonSpy = this.sandbox.spy(cookieMgr, 'set');
diff --git a/AISKU/Tests/Unit/src/SpanContextPropagation.Tests.ts b/AISKU/Tests/Unit/src/SpanContextPropagation.Tests.ts
new file mode 100644
index 000000000..8de152cfe
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanContextPropagation.Tests.ts
@@ -0,0 +1,733 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { IReadableSpan, IDistributedTraceContext, ITelemetryItem, asString } from "@microsoft/applicationinsights-core-js";
+import { createPromise, IPromise } from '@nevware21/ts-async';
+
+export class SpanContextPropagationTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${SpanContextPropagationTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "SpanContextPropagationTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanContextPropagationTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addParentChildRelationshipTests();
+ this.addMultiLevelHierarchyTests();
+ this.addSiblingSpanTests();
+ this.addAsyncBoundaryTests();
+ this.addContextPropagationTests();
+ }
+
+ private addParentChildRelationshipTests(): void {
+ this.testCase({
+ name: "ParentChild: child span should inherit parent's traceId",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-span");
+ Assert.ok(parentSpan, "Parent span should be created");
+
+ // Act
+ const parentContext = parentSpan!.spanContext();
+ const childSpan = this._ai.startSpan("child-span", undefined, parentContext);
+ const childContext = childSpan!.spanContext();
+
+ // Assert
+ Assert.equal(childContext.traceId, parentContext.traceId, "Child span should inherit parent's traceId");
+ Assert.notEqual(childContext.spanId, parentContext.spanId, "Child span should have different spanId from parent");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ParentChild: child span should have unique spanId",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-unique-id");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act
+ const child1 = this._ai.startSpan("child-1", undefined, parentContext);
+ const child2 = this._ai.startSpan("child-2", undefined, parentContext);
+
+ const child1Context = child1!.spanContext();
+ const child2Context = child2!.spanContext();
+
+ // Assert
+ Assert.notEqual(child1Context.spanId, child2Context.spanId,
+ "Sibling children should have unique spanIds");
+ Assert.notEqual(child1Context.spanId, parentContext.spanId,
+ "Child 1 spanId should differ from parent");
+ Assert.notEqual(child2Context.spanId, parentContext.spanId,
+ "Child 2 spanId should differ from parent");
+
+ // Cleanup
+ child1?.end();
+ child2?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ParentChild: child spans created via getTraceCtx",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-via-getTraceCtx");
+
+ this._ai.setActiveSpan(parentSpan!);
+
+ // Act - Use getTraceCtx to get current context
+ const currentContext = this._ai.getTraceCtx();
+ const childSpan = this._ai.startSpan("child-via-getTraceCtx", undefined, currentContext || undefined);
+
+ // Assert
+ const parentContext = parentSpan!.spanContext();
+ const childContext = childSpan!.spanContext();
+
+ Assert.equal(childContext.traceId, parentContext.traceId,
+ "Child should inherit traceId via getTraceCtx");
+ Assert.notEqual(childContext.spanId, parentContext.spanId,
+ "Child should have unique spanId");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ParentChild: parent context should preserve traceFlags",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-traceflags");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act
+ const childSpan = this._ai.startSpan("child-traceflags", undefined, parentContext);
+ const childContext = childSpan!.spanContext();
+
+ // Assert
+ Assert.equal(childContext.traceFlags, parentContext.traceFlags,
+ "Child should preserve parent's traceFlags");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ParentChild: parent context should preserve traceState if present",
+ test: () => {
+ // Arrange - Create parent span
+ const parentSpan = this._ai.startSpan("parent-tracestate");
+ const parentContext = parentSpan!.spanContext();
+
+ // Manually set traceState (if the implementation supports it)
+ if (parentContext.traceState !== undefined) {
+ // Act
+ const childSpan = this._ai.startSpan("child-tracestate", undefined, parentContext);
+ const childContext = childSpan!.spanContext();
+
+ // Assert
+ Assert.equal(asString(childContext.traceState), asString(parentContext.traceState),
+ "Child should preserve parent's traceState");
+
+ // Cleanup
+ childSpan?.end();
+ }
+
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ParentChild: multiple children from same parent share traceId",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-multiple-children");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act - Create multiple children
+ const children: IReadableSpan[] = [];
+ for (let i = 0; i < 5; i++) {
+ const child = this._ai.startSpan(`child-${i}`, undefined, parentContext);
+ if (child) {
+ children.push(child);
+ }
+ }
+
+ // Assert
+ children.forEach((child, index) => {
+ const childContext = child.spanContext();
+ Assert.equal(childContext.traceId, parentContext.traceId,
+ `Child ${index} should have parent's traceId`);
+ });
+
+ // All children should have unique spanIds
+ for (let i = 0; i < children.length; i++) {
+ for (let j = i + 1; j < children.length; j++) {
+ const ctx1 = children[i].spanContext();
+ const ctx2 = children[j].spanContext();
+ Assert.notEqual(ctx1.spanId, ctx2.spanId,
+ `Child ${i} and child ${j} should have different spanIds`);
+ }
+ }
+
+ // Cleanup
+ children.forEach(child => child.end());
+ parentSpan?.end();
+ }
+ });
+ }
+
+ private addMultiLevelHierarchyTests(): void {
+ this.testCase({
+ name: "MultiLevel: grandchild inherits root traceId",
+ test: () => {
+ // Arrange & Act - Create 3-level hierarchy
+ const rootSpan = this._ai.startSpan("root-span");
+ const rootContext = rootSpan!.spanContext();
+
+ const childSpan = this._ai.startSpan("child-span", undefined, rootContext);
+ const childContext = childSpan!.spanContext();
+
+ const grandchildSpan = this._ai.startSpan("grandchild-span", undefined, childContext);
+ const grandchildContext = grandchildSpan!.spanContext();
+
+ // Assert
+ Assert.equal(childContext.traceId, rootContext.traceId,
+ "Child should have root's traceId");
+ Assert.equal(grandchildContext.traceId, rootContext.traceId,
+ "Grandchild should have root's traceId");
+
+ Assert.notEqual(childContext.spanId, rootContext.spanId,
+ "Child should have unique spanId");
+ Assert.notEqual(grandchildContext.spanId, childContext.spanId,
+ "Grandchild should have unique spanId");
+ Assert.notEqual(grandchildContext.spanId, rootContext.spanId,
+ "Grandchild spanId should differ from root");
+
+ // Cleanup
+ grandchildSpan?.end();
+ childSpan?.end();
+ rootSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "MultiLevel: deep hierarchy maintains trace consistency",
+ test: () => {
+ // Arrange - Create deep hierarchy (5 levels)
+ const spans: IReadableSpan[] = [];
+
+ // Act - Create root
+ const rootSpan = this._ai.startSpan("level-0-root");
+ spans.push(rootSpan!);
+
+ // Create nested spans
+ for (let i = 1; i <= 4; i++) {
+ const parentContext = spans[i - 1].spanContext();
+ const childSpan = this._ai.startSpan(`level-${i}`, undefined, parentContext);
+ spans.push(childSpan!);
+ }
+
+ // Assert - All spans share same traceId
+ const rootTraceId = spans[0].spanContext().traceId;
+ spans.forEach((span, index) => {
+ const context = span.spanContext();
+ Assert.equal(context.traceId, rootTraceId,
+ `Level ${index} should have root traceId`);
+ });
+
+ // All spans should have unique spanIds
+ const spanIds = spans.map(span => span.spanContext().spanId);
+ const uniqueSpanIds = new Set(spanIds);
+ Assert.equal(uniqueSpanIds.size, spans.length,
+ "All spans should have unique spanIds");
+
+ // Cleanup
+ for (let i = spans.length - 1; i >= 0; i--) {
+ spans[i].end();
+ }
+ }
+ });
+
+ this.testCase({
+ name: "MultiLevel: intermediate span can be parent to multiple children",
+ test: () => {
+ // Arrange - Create hierarchy with branching
+ const rootSpan = this._ai.startSpan("root");
+ const rootContext = rootSpan!.spanContext();
+
+ const intermediateSpan = this._ai.startSpan("intermediate", undefined, rootContext);
+ const intermediateContext = intermediateSpan!.spanContext();
+
+ // Act - Create multiple children from intermediate
+ const leaf1 = this._ai.startSpan("leaf-1", undefined, intermediateContext);
+ const leaf2 = this._ai.startSpan("leaf-2", undefined, intermediateContext);
+ const leaf3 = this._ai.startSpan("leaf-3", undefined, intermediateContext);
+
+ // Assert
+ const leaf1Context = leaf1!.spanContext();
+ const leaf2Context = leaf2!.spanContext();
+ const leaf3Context = leaf3!.spanContext();
+
+ // All share same traceId
+ Assert.equal(leaf1Context.traceId, rootContext.traceId,
+ "Leaf 1 should have root traceId");
+ Assert.equal(leaf2Context.traceId, rootContext.traceId,
+ "Leaf 2 should have root traceId");
+ Assert.equal(leaf3Context.traceId, rootContext.traceId,
+ "Leaf 3 should have root traceId");
+
+ // All have unique spanIds
+ Assert.notEqual(leaf1Context.spanId, leaf2Context.spanId,
+ "Leaf 1 and 2 should have different spanIds");
+ Assert.notEqual(leaf2Context.spanId, leaf3Context.spanId,
+ "Leaf 2 and 3 should have different spanIds");
+ Assert.notEqual(leaf1Context.spanId, leaf3Context.spanId,
+ "Leaf 1 and 3 should have different spanIds");
+
+ // Cleanup
+ leaf3?.end();
+ leaf2?.end();
+ leaf1?.end();
+ intermediateSpan?.end();
+ rootSpan?.end();
+ }
+ });
+ }
+
+ private addSiblingSpanTests(): void {
+ this.testCase({
+ name: "Siblings: spans with same parent have same traceId",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-for-siblings");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act - Create sibling spans
+ const sibling1 = this._ai.startSpan("sibling-1", undefined, parentContext);
+ const sibling2 = this._ai.startSpan("sibling-2", undefined, parentContext);
+ const sibling3 = this._ai.startSpan("sibling-3", undefined, parentContext);
+
+ // Assert
+ const ctx1 = sibling1!.spanContext();
+ const ctx2 = sibling2!.spanContext();
+ const ctx3 = sibling3!.spanContext();
+
+ Assert.equal(ctx1.traceId, parentContext.traceId,
+ "Sibling 1 should have parent's traceId");
+ Assert.equal(ctx2.traceId, parentContext.traceId,
+ "Sibling 2 should have parent's traceId");
+ Assert.equal(ctx3.traceId, parentContext.traceId,
+ "Sibling 3 should have parent's traceId");
+
+ // Cleanup
+ sibling3?.end();
+ sibling2?.end();
+ sibling1?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Siblings: independent root spans have different traceIds",
+ test: () => {
+ // Act - Create independent root spans
+ const root1 = this._ai.startSpan("independent-root-1", { root: true });
+ const root2 = this._ai.startSpan("independent-root-2", { root: true });
+ const root3 = this._ai.startSpan("independent-root-3", { root: true });
+
+ // Assert
+ const ctx1 = root1!.spanContext();
+ const ctx2 = root2!.spanContext();
+ const ctx3 = root3!.spanContext();
+
+ Assert.notEqual(ctx1.traceId, ctx2.traceId,
+ "Independent root 1 and 2 should have different traceIds");
+ Assert.notEqual(ctx2.traceId, ctx3.traceId,
+ "Independent root 2 and 3 should have different traceIds");
+ Assert.notEqual(ctx1.traceId, ctx3.traceId,
+ "Independent root 1 and 3 should have different traceIds");
+
+ // Cleanup
+ root3?.end();
+ root2?.end();
+ root1?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Siblings: sibling spans have unique spanIds",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-unique-siblings");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act - Create many sibling spans
+ const siblings: IReadableSpan[] = [];
+ for (let i = 0; i < 10; i++) {
+ const sibling = this._ai.startSpan(`sibling-${i}`, undefined, parentContext);
+ if (sibling) {
+ siblings.push(sibling);
+ }
+ }
+
+ // Assert - All spanIds should be unique
+ const spanIds = siblings.map(s => s.spanContext().spanId);
+ const uniqueSpanIds = new Set(spanIds);
+ Assert.equal(uniqueSpanIds.size, siblings.length,
+ "All sibling spans should have unique spanIds");
+
+ // Cleanup
+ siblings.forEach(s => s.end());
+ parentSpan?.end();
+ }
+ });
+ }
+
+ private addAsyncBoundaryTests(): void {
+ this.testCase({
+ name: "AsyncBoundary: context can be captured and used across async operations",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("async-root");
+ const capturedContext = rootSpan!.spanContext();
+
+ // Act - Simulate async boundary by creating child later
+ return createPromise((resolve) => {
+ setTimeout(() => {
+ // Create child span using captured context
+ const childSpan = this._ai.startSpan("async-child", undefined, capturedContext);
+ const childContext = childSpan!.spanContext();
+
+ // Assert
+ Assert.equal(childContext.traceId, capturedContext.traceId,
+ "Child created after async boundary should have parent's traceId");
+
+ // Cleanup
+ childSpan?.end();
+ rootSpan?.end();
+ resolve();
+ }, 10);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "AsyncBoundary: getTraceCtx can capture context for async operations",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("async-getTraceCtx-root");
+ this._ai.setActiveSpan(rootSpan!);
+
+ // Capture context using getTraceCtx
+ const capturedContext = this._ai.getTraceCtx();
+
+ // Act - Simulate async operation
+ return createPromise((resolve) => {
+ setTimeout(() => {
+ // Use captured context in async boundary
+ const asyncSpan = this._ai.startSpan("async-operation", undefined, capturedContext || undefined);
+ const asyncContext = asyncSpan!.spanContext();
+
+ // Assert
+ Assert.equal(asyncContext.traceId, capturedContext.traceId, "Async span should inherit captured traceId");
+
+ // Cleanup
+ asyncSpan?.end();
+ rootSpan?.end();
+ resolve();
+ }, 10);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "AsyncBoundary: nested async operations maintain trace",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("nested-async-root");
+ const rootContext = rootSpan!.spanContext();
+
+ // Act - Chain async operations
+ return createPromise((resolve) => {
+ setTimeout(() => {
+ const child1 = this._ai.startSpan("async-child-1", undefined, rootContext);
+ const child1Context = child1!.spanContext();
+
+ setTimeout(() => {
+ const child2 = this._ai.startSpan("async-child-2", undefined, child1Context);
+ const child2Context = child2!.spanContext();
+
+ // Assert
+ Assert.equal(child1Context.traceId, rootContext.traceId,
+ "First async child should have root traceId");
+ Assert.equal(child2Context.traceId, rootContext.traceId,
+ "Second async child should have root traceId");
+
+ // Cleanup
+ child2?.end();
+ child1?.end();
+ rootSpan?.end();
+ resolve();
+ }, 10);
+ }, 10);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "AsyncBoundary: parallel async operations share traceId",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("parallel-async-root");
+ const rootContext = rootSpan!.spanContext();
+
+ // Act - Create parallel async operations
+ const promises: IPromise[] = [];
+ const childContexts: IDistributedTraceContext[] = [];
+
+ for (let i = 0; i < 3; i++) {
+ const promise = createPromise((resolve) => {
+ setTimeout(() => {
+ const childSpan = this._ai.startSpan(`parallel-child-${i}`, undefined, rootContext);
+ childContexts.push(childSpan!.spanContext());
+ childSpan?.end();
+ resolve();
+ }, 10 + i * 5);
+ });
+ promises.push(promise);
+ }
+
+ return Promise.all(promises).then(() => {
+ // Assert - All parallel children should share root traceId
+ childContexts.forEach((ctx, index) => {
+ Assert.equal(ctx.traceId, rootContext.traceId,
+ `Parallel child ${index} should have root traceId`);
+ });
+
+ // All should have unique spanIds
+ const spanIds = childContexts.map(ctx => ctx.spanId);
+ const uniqueSpanIds = new Set(spanIds);
+ Assert.equal(uniqueSpanIds.size, childContexts.length,
+ "Parallel children should have unique spanIds");
+
+ // Cleanup
+ rootSpan?.end();
+ });
+ }
+ });
+ }
+
+ private addContextPropagationTests(): void {
+ this.testCase({
+ name: "ContextPropagation: explicit parent context overrides active context",
+ test: () => {
+ // Arrange - Create two independent traces
+ const trace1Root = this._ai.startSpan("trace-1-root", { root: true });
+ const trace2Root = this._ai.startSpan("trace-2-root", { root: true });
+
+ this._ai.setActiveSpan(trace1Root!);
+
+ // Act - Create child with explicit trace2 parent
+ const trace2Context = trace2Root!.spanContext();
+ const childSpan = this._ai.startSpan("explicit-parent-child", undefined, trace2Context);
+ const childContext = childSpan!.spanContext();
+
+ // Assert - Child should belong to trace2, not active trace1
+ Assert.equal(childContext.traceId, trace2Context.traceId,
+ "Explicit parent context should override active context");
+ Assert.notEqual(childContext.traceId, trace1Root!.spanContext().traceId,
+ "Child should not belong to active trace");
+
+ // Cleanup
+ childSpan?.end();
+ trace2Root?.end();
+ trace1Root?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ContextPropagation: spans without parent create new trace",
+ test: () => {
+ // Act - Create spans without explicit parent
+ const span1 = this._ai.startSpan("no-parent-1");
+ const span2 = this._ai.startSpan("no-parent-2");
+
+ const ctx1 = span1!.spanContext();
+ const ctx2 = span2!.spanContext();
+
+ // Assert - Should create independent traces or share active context
+ // (depends on implementation - both are valid)
+ Assert.ok(ctx1.traceId, "Span 1 should have traceId");
+ Assert.ok(ctx2.traceId, "Span 2 should have traceId");
+ Assert.ok(ctx1.spanId !== ctx2.spanId,
+ "Spans should have unique spanIds");
+
+ // Cleanup
+ span2?.end();
+ span1?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ContextPropagation: root option creates new trace",
+ test: () => {
+ // Arrange - Create parent span
+ const parentSpan = this._ai.startSpan("existing-parent");
+ this._ai.setActiveSpan(parentSpan!);
+
+ // Act - Create root span (should ignore active parent)
+ const rootSpan = this._ai.startSpan("new-root", { root: true });
+
+ const parentContext = parentSpan!.spanContext();
+ const rootContext = rootSpan!.spanContext();
+
+ // Assert - Root span should have different traceId
+ Assert.notEqual(rootContext.traceId, parentContext.traceId,
+ "Root option should create new independent trace");
+
+ // Cleanup
+ rootSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ContextPropagation: context with all required fields propagates correctly",
+ test: () => {
+ // Arrange - Create context with all fields
+ const parentSpan = this._ai.startSpan("full-context-parent");
+ const parentContext = parentSpan!.spanContext();
+
+ // Act - Create child
+ const childSpan = this._ai.startSpan("full-context-child", undefined, parentContext);
+ const childContext = childSpan!.spanContext();
+
+ // Assert - All fields should be present
+ Assert.ok(childContext.traceId, "Child should have traceId");
+ Assert.ok(childContext.spanId, "Child should have spanId");
+ Assert.equal(childContext.traceFlags, parentContext.traceFlags,
+ "Child should have traceFlags");
+
+ Assert.equal(childContext.traceId, parentContext.traceId,
+ "TraceId should propagate");
+ Assert.equal(childContext.traceFlags, parentContext.traceFlags,
+ "TraceFlags should propagate");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ContextPropagation: recording attribute propagates independently",
+ test: () => {
+ // Arrange - Create recording parent
+ const recordingParent = this._ai.startSpan("recording-parent", { recording: true });
+ const recordingContext = recordingParent!.spanContext();
+
+ // Act - Create non-recording child from recording parent
+ const nonRecordingChild = this._ai.startSpan("non-recording-child",
+ { recording: false }, recordingContext);
+
+ // Assert - Recording is per-span, not propagated
+ Assert.ok(recordingParent!.isRecording(),
+ "Parent should be recording");
+ Assert.ok(!nonRecordingChild!.isRecording(),
+ "Child should not be recording despite recording parent");
+
+ // But traceId should still propagate
+ Assert.equal(nonRecordingChild!.spanContext().traceId, recordingContext.traceId,
+ "TraceId should propagate regardless of recording state");
+
+ // Cleanup
+ nonRecordingChild?.end();
+ recordingParent?.end();
+ }
+ });
+
+ this.testCase({
+ name: "ContextPropagation: span attributes do not propagate to children",
+ test: () => {
+ // Arrange - Create parent with attributes
+ const parentAttrs = {
+ "parent.attr1": "value1",
+ "parent.attr2": "value2"
+ };
+ const parentSpan = this._ai.startSpan("parent-with-attrs",
+ { attributes: parentAttrs });
+ const parentContext = parentSpan!.spanContext();
+
+ // Act - Create child with different attributes
+ const childAttrs = {
+ "child.attr1": "childValue1"
+ };
+ const childSpan = this._ai.startSpan("child-with-attrs",
+ { attributes: childAttrs }, parentContext);
+
+ // Assert - Attributes are per-span, not inherited
+ Assert.ok(parentSpan!.attributes["parent.attr1"] === "value1",
+ "Parent should have its attributes");
+ Assert.ok(childSpan!.attributes["child.attr1"] === "childValue1",
+ "Child should have its attributes");
+ Assert.ok(!childSpan!.attributes["parent.attr1"],
+ "Child should not inherit parent's attributes");
+
+ // But context should propagate
+ Assert.equal(childSpan!.spanContext().traceId, parentContext.traceId,
+ "TraceId should propagate");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanE2E.Tests.ts b/AISKU/Tests/Unit/src/SpanE2E.Tests.ts
new file mode 100644
index 000000000..e9389d0a5
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanE2E.Tests.ts
@@ -0,0 +1,751 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import { eOTelSpanKind, eOTelSpanStatusCode } from "@microsoft/applicationinsights-core-js";
+
+/**
+ * E2E Tests for Span APIs that send real telemetry to Breeze endpoint
+ *
+ * These tests can be run manually to verify telemetry appears correctly in the Azure Portal:
+ * 1. Set MANUAL_E2E_TEST to true
+ * 2. Replace the instrumentationKey with a valid test iKey
+ * 3. Run the tests
+ * 4. Check the Azure Portal for the telemetry within 1-2 minutes
+ *
+ * Look for:
+ * - Dependencies in the "Performance" blade
+ * - Requests in the "Performance" blade
+ * - Custom properties and measurements
+ * - Distributed trace correlation
+ * - End-to-end transaction view
+ */
+export class SpanE2ETests extends AITestClass {
+ // Set to true to actually send telemetry to Breeze for manual validation
+ private static readonly MANUAL_E2E_TEST = false;
+
+ // Replace with your test instrumentation key for manual E2E testing
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${SpanE2ETests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+
+ constructor(testName?: string) {
+ super(testName || "SpanE2ETests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = !SpanE2ETests.MANUAL_E2E_TEST;
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanE2ETests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ disableFetchTracking: false,
+ enableAutoRouteTracking: true,
+ disableExceptionTracking: false,
+ maxBatchInterval: 1000, // Send quickly for manual testing
+ enableDebug: true,
+ loggingLevelConsole: 2 // Show warnings and errors
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("=== MANUAL E2E TEST MODE ===");
+ console.log("Telemetry will be sent to Breeze endpoint");
+ console.log("Check Azure Portal in 1-2 minutes");
+ console.log("Instrumentation Key:", SpanE2ETests._instrumentationKey);
+ console.log("============================");
+ }
+ } catch (e) {
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ // Flush any pending telemetry before cleanup
+ this._ai.flush();
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addE2EBasicSpanTests();
+ this.addE2EDistributedTraceTests();
+ this.addE2EHttpDependencyTests();
+ this.addE2EDatabaseDependencyTests();
+ this.addE2EComplexScenarioTests();
+ }
+
+ private addE2EBasicSpanTests(): void {
+ this.testCase({
+ name: "E2E: Basic CLIENT span creates RemoteDependency in portal",
+ test: () => {
+ // This will appear in the Azure Portal under Performance -> Dependencies
+ const span = this._ai.startSpan("E2E-BasicClientSpan", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.scenario": "basic-client",
+ "test.timestamp": new Date().toISOString(),
+ "test.type": "manual-validation",
+ "custom.property": "This should appear in custom properties"
+ }
+ });
+
+ // Simulate some work
+ if (span) {
+ span.setAttribute("work.completed", true);
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+
+ // Flush to ensure it's sent
+ this._ai.flush();
+
+ Assert.ok(span, "Span should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Basic CLIENT span sent - Check Azure Portal Dependencies");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Basic SERVER span creates Request in portal",
+ test: () => {
+ // This will appear in the Azure Portal under Performance -> Requests
+ const span = this._ai.startSpan("E2E-BasicServerSpan", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://example.com/api/test",
+ "http.status_code": 200,
+ "test.scenario": "basic-server",
+ "test.timestamp": new Date().toISOString()
+ }
+ });
+
+ if (span) {
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(span, "Span should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Basic SERVER span sent - Check Azure Portal Requests");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Failed span shows as error in portal",
+ test: () => {
+ const span = this._ai.startSpan("E2E-FailedOperation", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.scenario": "failure-case",
+ "test.timestamp": new Date().toISOString()
+ }
+ });
+
+ if (span) {
+ // Simulate a failure
+ span.setAttribute("error.type", "TimeoutError");
+ span.setAttribute("error.message", "Operation timed out after 5000ms");
+ span.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Operation failed due to timeout"
+ });
+ span.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(span, "Span should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Failed span sent - Should show success=false in portal");
+ }
+ }
+ });
+ }
+
+ private addE2EDistributedTraceTests(): void {
+ this.testCase({
+ name: "E2E: Parent-child span relationship visible in portal",
+ test: () => {
+ // Create parent span
+ const parentSpan = this._ai.startSpan("E2E-ParentOperation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.scenario": "distributed-trace",
+ "test.timestamp": new Date().toISOString(),
+ "operation.level": "parent"
+ }
+ });
+
+ const parentContext = parentSpan?.spanContext();
+
+ // Create child span with explicit parent
+ const childSpan1 = this._ai.startSpan("E2E-ChildOperation1", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "operation.level": "child",
+ "child.index": 1
+ }
+ }, parentContext);
+
+ if (childSpan1) {
+ childSpan1.setAttribute("http.url", "https://api.example.com/users");
+ childSpan1.setAttribute("http.method", "GET");
+ childSpan1.setStatus({ code: eOTelSpanStatusCode.OK });
+ childSpan1.end();
+ }
+
+ // Create another child
+ const childSpan2 = this._ai.startSpan("E2E-ChildOperation2", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "operation.level": "child",
+ "child.index": 2
+ }
+ }, parentContext);
+
+ if (childSpan2) {
+ childSpan2.setAttribute("http.url", "https://api.example.com/orders");
+ childSpan2.setAttribute("http.method", "POST");
+ childSpan2.setStatus({ code: eOTelSpanStatusCode.OK });
+ childSpan2.end();
+ }
+
+ // End parent
+ if (parentSpan) {
+ parentSpan.setAttribute("children.count", 2);
+ parentSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ parentSpan.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(parentSpan && childSpan1 && childSpan2, "All spans should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Distributed trace sent - Check End-to-End Transaction view");
+ console.log(" Parent operation.id:", parentContext?.traceId);
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Nested span hierarchy (3 levels) visible in portal",
+ test: () => {
+ // Level 1: Root
+ const rootSpan = this._ai.startSpan("E2E-RootOperation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.scenario": "nested-hierarchy",
+ "test.timestamp": new Date().toISOString(),
+ "span.level": 1
+ }
+ });
+
+ const rootContext = rootSpan?.spanContext();
+
+ // Level 2: Child
+ const level2Span = this._ai.startSpan("E2E-Level2Operation", {
+ kind: eOTelSpanKind.INTERNAL,
+ attributes: {
+ "span.level": 2
+ }
+ }, rootContext);
+
+ const level2Context = level2Span?.spanContext();
+
+ // Level 3: Grandchild
+ const level3Span = this._ai.startSpan("E2E-Level3Operation", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "span.level": 3,
+ "http.url": "https://api.example.com/deep-call"
+ }
+ }, level2Context);
+
+ // End in reverse order (child first, parent last)
+ if (level3Span) {
+ level3Span.setStatus({ code: eOTelSpanStatusCode.OK });
+ level3Span.end();
+ }
+
+ if (level2Span) {
+ level2Span.setStatus({ code: eOTelSpanStatusCode.OK });
+ level2Span.end();
+ }
+
+ if (rootSpan) {
+ rootSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ rootSpan.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(rootSpan && level2Span && level3Span, "All spans should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ 3-level nested trace sent - Check transaction timeline");
+ }
+ }
+ });
+ }
+
+ private addE2EHttpDependencyTests(): void {
+ this.testCase({
+ name: "E2E: HTTP dependency with full details in portal",
+ test: () => {
+ const span = this._ai.startSpan("E2E-HTTPDependency", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://api.example.com/v1/users/create",
+ "http.status_code": 201,
+ "http.request.header.content-type": "application/json",
+ "http.response.header.content-length": "1234",
+ "test.scenario": "http-dependency",
+ "test.timestamp": new Date().toISOString(),
+ "request.body.size": 512,
+ "response.time.ms": 145
+ }
+ });
+
+ if (span) {
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(span, "Span should be created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ HTTP dependency sent - Check Dependencies with full HTTP details");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: HTTP dependency with various status codes in portal",
+ test: () => {
+ const statusCodes = [200, 201, 204, 400, 401, 403, 404, 500, 502, 503];
+
+ for (const statusCode of statusCodes) {
+ const isSuccess = statusCode >= 200 && statusCode < 400;
+ const span = this._ai.startSpan(`E2E-HTTP-${statusCode}`, {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": `https://api.example.com/status/${statusCode}`,
+ "http.status_code": statusCode,
+ "test.scenario": "http-status-codes",
+ "test.timestamp": new Date().toISOString()
+ }
+ });
+
+ if (span) {
+ span.setStatus({
+ code: isSuccess ? eOTelSpanStatusCode.OK : eOTelSpanStatusCode.ERROR
+ });
+ span.end();
+ }
+ }
+
+ this._ai.flush();
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Multiple HTTP status codes sent - Check success/failure in portal");
+ }
+
+ Assert.ok(true, "Multiple status codes tested");
+ }
+ });
+ }
+
+ private addE2EDatabaseDependencyTests(): void {
+ this.testCase({
+ name: "E2E: Database dependencies appear in portal",
+ test: () => {
+ const databases = [
+ { system: "mysql", statement: "SELECT * FROM users WHERE id = ?", name: "production_db" },
+ { system: "postgresql", statement: "INSERT INTO logs (message, level) VALUES ($1, $2)", name: "logs_db" },
+ { system: "mongodb", statement: "db.products.find({category: 'electronics'})", name: "catalog_db" },
+ { system: "redis", statement: "GET user:session:abc123", name: "cache_db" },
+ { system: "mssql", statement: "EXEC sp_GetUserOrders @UserId=123", name: "orders_db" }
+ ];
+
+ for (const db of databases) {
+ const span = this._ai.startSpan(`E2E-DB-${db.system}`, {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "db.system": db.system,
+ "db.statement": db.statement,
+ "db.name": db.name,
+ "db.user": "app_user",
+ "net.peer.name": `${db.system}.example.com`,
+ "net.peer.port": 5432,
+ "test.scenario": "database-dependencies",
+ "test.timestamp": new Date().toISOString()
+ }
+ });
+
+ if (span) {
+ span.setAttribute("db.rows.affected", 42);
+ span.setAttribute("db.duration.ms", 23);
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+ }
+
+ this._ai.flush();
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Database dependencies sent - Check Dependencies for SQL/NoSQL types");
+ }
+
+ Assert.ok(true, "Database dependencies tested");
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Database slow query marked appropriately",
+ test: () => {
+ const span = this._ai.startSpan("E2E-SlowDatabaseQuery", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "db.system": "postgresql",
+ "db.statement": "SELECT * FROM orders JOIN users ON orders.user_id = users.id WHERE created_at > NOW() - INTERVAL '30 days'",
+ "db.name": "analytics_db",
+ "test.scenario": "slow-query",
+ "test.timestamp": new Date().toISOString(),
+ "db.query.execution.plan": "SeqScan on orders (cost=0.00..1000.00 rows=10000)",
+ "db.slow.query": true,
+ "db.duration.ms": 5432
+ }
+ });
+
+ if (span) {
+ // Mark as warning (not error, but slow)
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.setAttribute("performance.warning", "Query exceeded 1000ms threshold");
+ span.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(span, "Slow query span created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Slow database query sent - Check duration in portal");
+ }
+ }
+ });
+ }
+
+ private addE2EComplexScenarioTests(): void {
+ this.testCase({
+ name: "E2E: Complex e-commerce checkout scenario in portal",
+ test: () => {
+ // Simulate a complete e-commerce checkout flow with multiple dependencies
+ const timestamp = new Date().toISOString();
+
+ // 1. Initial checkout request
+ const checkoutSpan = this._ai.startSpan("E2E-CheckoutRequest", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.scenario": "complex-ecommerce",
+ "test.timestamp": timestamp,
+ "http.method": "POST",
+ "http.url": "https://shop.example.com/api/checkout",
+ "http.status_code": 200,
+ "user.id": "user_12345",
+ "cart.items.count": 3,
+ "cart.total.amount": 299.97
+ }
+ });
+
+ const checkoutContext = checkoutSpan?.spanContext();
+
+ // 2. Validate inventory
+ const inventorySpan = this._ai.startSpan("E2E-ValidateInventory", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://inventory-api.example.com/validate",
+ "http.status_code": 200,
+ "items.validated": 3
+ }
+ }, checkoutContext);
+
+ if (inventorySpan) {
+ inventorySpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ inventorySpan.end();
+ }
+
+ // 3. Calculate shipping
+ const shippingSpan = this._ai.startSpan("E2E-CalculateShipping", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://shipping-api.example.com/calculate",
+ "http.status_code": 200,
+ "shipping.method": "express",
+ "shipping.cost": 15.99
+ }
+ }, checkoutContext);
+
+ if (shippingSpan) {
+ shippingSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ shippingSpan.end();
+ }
+
+ // 4. Process payment
+ const paymentSpan = this._ai.startSpan("E2E-ProcessPayment", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://payments.example.com/charge",
+ "http.status_code": 200,
+ "payment.method": "credit_card",
+ "payment.amount": 315.96,
+ "payment.currency": "USD"
+ }
+ }, checkoutContext);
+
+ if (paymentSpan) {
+ paymentSpan.setAttribute("payment.processor", "stripe");
+ paymentSpan.setAttribute("payment.transaction.id", "txn_abc123xyz");
+ paymentSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ paymentSpan.end();
+ }
+
+ // 5. Create order in database
+ const createOrderSpan = this._ai.startSpan("E2E-CreateOrder", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "db.system": "postgresql",
+ "db.statement": "INSERT INTO orders (user_id, total, status) VALUES ($1, $2, $3) RETURNING id",
+ "db.name": "orders_db",
+ "db.operation": "INSERT"
+ }
+ }, checkoutContext);
+
+ if (createOrderSpan) {
+ createOrderSpan.setAttribute("order.id", "ord_98765");
+ createOrderSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ createOrderSpan.end();
+ }
+
+ // 6. Send confirmation email
+ const emailSpan = this._ai.startSpan("E2E-SendConfirmationEmail", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://email-service.example.com/send",
+ "http.status_code": 202,
+ "email.recipient": "user@example.com",
+ "email.template": "order-confirmation"
+ }
+ }, checkoutContext);
+
+ if (emailSpan) {
+ emailSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ emailSpan.end();
+ }
+
+ // 7. Update cache
+ const cacheSpan = this._ai.startSpan("E2E-UpdateCache", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "db.system": "redis",
+ "db.statement": "SET user:12345:last_order ord_98765 EX 86400",
+ "cache.operation": "set",
+ "cache.key": "user:12345:last_order"
+ }
+ }, checkoutContext);
+
+ if (cacheSpan) {
+ cacheSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ cacheSpan.end();
+ }
+
+ // Complete checkout
+ if (checkoutSpan) {
+ checkoutSpan.setAttribute("checkout.status", "completed");
+ checkoutSpan.setAttribute("order.id", "ord_98765");
+ checkoutSpan.setAttribute("dependencies.count", 7);
+ checkoutSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ checkoutSpan.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(checkoutSpan, "Checkout span created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Complex e-commerce scenario sent");
+ console.log(" Trace ID:", checkoutContext?.traceId);
+ console.log(" Check End-to-End Transaction view for complete flow");
+ console.log(" Expected: 1 Request + 7 Dependencies");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Mixed success and failure scenario in portal",
+ test: () => {
+ const timestamp = new Date().toISOString();
+
+ // Parent operation
+ const operationSpan = this._ai.startSpan("E2E-MixedResultsOperation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.scenario": "mixed-success-failure",
+ "test.timestamp": timestamp
+ }
+ });
+
+ const operationContext = operationSpan?.spanContext();
+
+ // Successful child 1
+ const successSpan1 = this._ai.startSpan("E2E-SuccessfulCall1", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.url": "https://api.example.com/service1",
+ "http.status_code": 200
+ }
+ }, operationContext);
+
+ if (successSpan1) {
+ successSpan1.setStatus({ code: eOTelSpanStatusCode.OK });
+ successSpan1.end();
+ }
+
+ // Failed child
+ const failedSpan = this._ai.startSpan("E2E-FailedCall", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.url": "https://api.example.com/service2",
+ "http.status_code": 503
+ }
+ }, operationContext);
+
+ if (failedSpan) {
+ failedSpan.setAttribute("error.type", "ServiceUnavailable");
+ failedSpan.setAttribute("retry.count", 3);
+ failedSpan.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Service temporarily unavailable"
+ });
+ failedSpan.end();
+ }
+
+ // Successful child 2 (after retry)
+ const successSpan2 = this._ai.startSpan("E2E-SuccessfulCall2", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.url": "https://api.example.com/service3",
+ "http.status_code": 200
+ }
+ }, operationContext);
+
+ if (successSpan2) {
+ successSpan2.setStatus({ code: eOTelSpanStatusCode.OK });
+ successSpan2.end();
+ }
+
+ // Parent partially successful
+ if (operationSpan) {
+ operationSpan.setAttribute("successful.calls", 2);
+ operationSpan.setAttribute("failed.calls", 1);
+ operationSpan.setAttribute("total.calls", 3);
+ operationSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ operationSpan.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(operationSpan, "Operation span created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Mixed success/failure scenario sent");
+ console.log(" Check for 2 successful + 1 failed dependency in transaction");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E: Span with rich custom properties for portal search",
+ test: () => {
+ const span = this._ai.startSpan("E2E-RichProperties", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.scenario": "rich-properties",
+ "test.timestamp": new Date().toISOString(),
+
+ // Business context
+ "business.tenant": "acme-corp",
+ "business.region": "us-west-2",
+ "business.environment": "production",
+
+ // User context
+ "user.id": "user_12345",
+ "user.email": "test@example.com",
+ "user.subscription": "premium",
+ "user.account.age.days": 456,
+
+ // Request context
+ "request.id": "req_abc123",
+ "request.source": "web-app",
+ "request.version": "v2.3.1",
+
+ // Performance metrics
+ "performance.db.queries": 5,
+ "performance.cache.hits": 3,
+ "performance.cache.misses": 2,
+ "performance.total.ms": 234,
+
+ // Feature flags
+ "feature.new.checkout": true,
+ "feature.ab.test.group": "variant-b",
+
+ // Custom measurements
+ "metrics.items.processed": 42,
+ "metrics.data.size.kb": 128
+ }
+ });
+
+ if (span) {
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ }
+
+ this._ai.flush();
+
+ Assert.ok(span, "Span with rich properties created");
+
+ if (SpanE2ETests.MANUAL_E2E_TEST) {
+ console.log("✓ Span with rich properties sent");
+ console.log(" Use Application Insights search to filter by custom properties");
+ console.log(" Example queries:");
+ console.log(" - customDimensions.business.tenant == 'acme-corp'");
+ console.log(" - customDimensions.user.subscription == 'premium'");
+ console.log(" - customDimensions.feature.new.checkout == true");
+ }
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanErrorHandling.Tests.ts b/AISKU/Tests/Unit/src/SpanErrorHandling.Tests.ts
new file mode 100644
index 000000000..d6079a02f
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanErrorHandling.Tests.ts
@@ -0,0 +1,768 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { IReadableSpan, eOTelSpanStatusCode, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+
+export class SpanErrorHandlingTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${SpanErrorHandlingTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "SpanErrorHandlingTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanErrorHandlingTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addInvalidSpanNameTests();
+ this.addInvalidAttributeTests();
+ this.addNullUndefinedInputTests();
+ this.addInvalidParentContextTests();
+ this.addInvalidOptionsTests();
+ this.addEdgeCaseTests();
+ }
+
+ private addInvalidSpanNameTests(): void {
+ this.testCase({
+ name: "SpanName: empty string name should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("");
+ span?.end();
+ }, "Empty string name should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "SpanName: null name should handle gracefully",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(null as any);
+ span?.end();
+ }, "Null name should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "SpanName: undefined name should handle gracefully",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(undefined as any);
+ span?.end();
+ }, "Undefined name should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "SpanName: very long name should be accepted",
+ test: () => {
+ // Arrange
+ const longName = "a".repeat(10000);
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(longName);
+ Assert.ok(span, "Should create span with long name");
+ span?.end();
+ }, "Very long name should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "SpanName: special characters in name should be accepted",
+ test: () => {
+ // Arrange
+ const specialNames = [
+ "span-with-dashes",
+ "span_with_underscores",
+ "span.with.dots",
+ "span/with/slashes",
+ "span:with:colons",
+ "span@with@at",
+ "span#with#hash",
+ "span$with$dollar"
+ ];
+
+ // Act & Assert
+ specialNames.forEach(name => {
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(name);
+ span?.end();
+ }, `Special character name '${name}' should not throw`);
+ });
+ }
+ });
+ }
+
+ private addInvalidAttributeTests(): void {
+ this.testCase({
+ name: "Attributes: null attribute value should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-attribute-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("nullable.attr", null);
+ }, "Setting null attribute should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: undefined attribute value should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-attribute-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("undefined.attr", undefined);
+ }, "Setting undefined attribute should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: empty string attribute key should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("empty-key-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("", "value");
+ }, "Empty string key should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: null attribute key should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-key-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute(null as any, "value");
+ }, "Null key should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: undefined attribute key should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-key-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute(undefined as any, "value");
+ }, "Undefined key should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: invalid attribute value types should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("invalid-type-test");
+
+ // Act & Assert - Test various invalid types
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("object.attr", { nested: "object" } as any);
+ span?.setAttribute("array.attr", [1, 2, 3] as any);
+ span?.setAttribute("function.attr", (() => {}) as any);
+ span?.setAttribute("symbol.attr", Symbol("test") as any);
+ }, "Invalid attribute types should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: setAttributes with null should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("setAttributes-null-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttributes(null as any);
+ }, "setAttributes with null should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: setAttributes with undefined should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("setAttributes-undefined-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttributes(undefined as any);
+ }, "setAttributes with undefined should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: setAttributes with invalid object should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("setAttributes-invalid-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttributes({
+ "valid": "value",
+ "null.value": null,
+ "undefined.value": undefined,
+ "object.value": { nested: "obj" } as any
+ });
+ }, "setAttributes with mixed valid/invalid should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+ }
+
+ private addNullUndefinedInputTests(): void {
+ this.testCase({
+ name: "NullUndefined: startSpan with null options should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("null-options-test", null as any);
+ span?.end();
+ }, "Null options should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: startSpan with undefined options should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("undefined-options-test", undefined);
+ span?.end();
+ }, "Undefined options should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: setStatus with null should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-status-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setStatus(null as any);
+ }, "setStatus with null should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: setStatus with undefined should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-status-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setStatus(undefined as any);
+ }, "setStatus with undefined should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: updateName with null should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-name-update-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.updateName(null as any);
+ }, "updateName with null should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: updateName with undefined should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-name-update-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.updateName(undefined as any);
+ }, "updateName with undefined should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: end with null time should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-end-time-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.end(null as any);
+ }, "end with null time should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: end with undefined time should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-end-time-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.end(undefined);
+ }, "end with undefined time should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: recordException with null should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("null-exception-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.recordException(null as any);
+ }, "recordException with null should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "NullUndefined: recordException with undefined should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("undefined-exception-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.recordException(undefined as any);
+ }, "recordException with undefined should not throw");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+ }
+
+ private addInvalidParentContextTests(): void {
+ this.testCase({
+ name: "ParentContext: null parent context should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("null-parent-test", undefined, null as any);
+ span?.end();
+ }, "Null parent context should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "ParentContext: undefined parent context should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("undefined-parent-test", undefined, undefined);
+ span?.end();
+ }, "Undefined parent context should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "ParentContext: invalid parent context object should not throw",
+ test: () => {
+ // Arrange - Create invalid context objects
+ const invalidContexts = [
+ {},
+ { traceId: "invalid" },
+ { spanId: "invalid" },
+ { traceId: "", spanId: "" },
+ { traceId: "123", spanId: "456" } // Too short
+ ];
+
+ // Act & Assert
+ invalidContexts.forEach((ctx, index) => {
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(`invalid-context-${index}`, undefined, ctx as any);
+ span?.end();
+ }, `Invalid context ${index} should not throw`);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "ParentContext: parent context with missing fields should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("missing-fields-test", undefined, {
+ traceId: "12345678901234567890123456789012"
+ // Missing spanId and traceFlags
+ } as any);
+ span?.end();
+ }, "Parent context with missing fields should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "ParentContext: parent context with wrong types should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("wrong-types-test", undefined, {
+ traceId: 123456789, // Should be string
+ spanId: 987654321, // Should be string
+ traceFlags: "invalid" // Should be number
+ } as any);
+ span?.end();
+ }, "Parent context with wrong types should not throw");
+ }
+ });
+ }
+
+ private addInvalidOptionsTests(): void {
+ this.testCase({
+ name: "Options: invalid kind value should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("invalid-kind-test", {
+ kind: 999 as any // Invalid kind value
+ });
+ span?.end();
+ }, "Invalid kind value should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: negative kind value should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("negative-kind-test", {
+ kind: -1 as any
+ });
+ span?.end();
+ }, "Negative kind value should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: null attributes in options should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("null-attrs-options-test", {
+ attributes: null as any
+ });
+ span?.end();
+ }, "Null attributes in options should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: undefined attributes in options should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("undefined-attrs-options-test", {
+ attributes: undefined
+ });
+ span?.end();
+ }, "Undefined attributes in options should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: invalid startTime should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("invalid-starttime-test", {
+ startTime: "invalid" as any
+ });
+ span?.end();
+ }, "Invalid startTime should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: negative startTime should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("negative-starttime-test", {
+ startTime: -1000
+ });
+ span?.end();
+ }, "Negative startTime should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: future startTime should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("future-starttime-test", {
+ startTime: Date.now() + 1000000
+ });
+ span?.end();
+ }, "Future startTime should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "Options: multiple invalid options should not throw",
+ test: () => {
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan("multi-invalid-options-test", {
+ kind: -999 as any,
+ attributes: null as any,
+ startTime: "invalid" as any,
+ recording: "maybe" as any,
+ root: "yes" as any
+ } as any);
+ span?.end();
+ }, "Multiple invalid options should not throw");
+ }
+ });
+ }
+
+ private addEdgeCaseTests(): void {
+ this.testCase({
+ name: "EdgeCase: operations on null span should not throw",
+ test: () => {
+ // Arrange - Force null span (though SDK shouldn't return null)
+ const span: IReadableSpan | null = null;
+
+ // Act & Assert - All operations should be safe
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("key", "value");
+ span?.setAttributes({ "key": "value" });
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.updateName("new-name");
+ span?.end();
+ span?.recordException(new Error("test"));
+ }, "Operations on null span should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: extremely large attribute count should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("large-attr-count-test");
+ const largeAttrs: any = {};
+ for (let i = 0; i < 1000; i++) {
+ largeAttrs[`attr_${i}`] = `value_${i}`;
+ }
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttributes(largeAttrs);
+ span?.end();
+ }, "Large attribute count should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: very long attribute values should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("long-attr-value-test");
+ const longValue = "x".repeat(100000);
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("long.attr", longValue);
+ span?.end();
+ }, "Very long attribute values should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: rapid successive operations should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("rapid-ops-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ for (let i = 0; i < 100; i++) {
+ span?.setAttribute(`rapid_${i}`, i);
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.updateName(`name_${i}`);
+ }
+ span?.end();
+ }, "Rapid successive operations should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: mixed valid and invalid operations should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("mixed-ops-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("valid", "value");
+ span?.setAttribute(null as any, "invalid-key");
+ span?.setAttribute("another.valid", 123);
+ span?.setAttribute("", "empty-key");
+ span?.setAttributes({ "good": "attr", "bad": null });
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.updateName(null as any);
+ span?.updateName("valid-name");
+ span?.end();
+ }, "Mixed valid and invalid operations should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: special Unicode characters should not throw",
+ test: () => {
+ // Arrange
+ const unicodeStrings = [
+ "Hello 世界",
+ "Emoji 😀🎉",
+ "RTL العربية",
+ "Combined ñ é ü",
+ "Zero-width\u200B\u200Ccharacters"
+ ];
+
+ // Act & Assert
+ unicodeStrings.forEach((str, index) => {
+ Assert.doesNotThrow(() => {
+ const span = this._ai.startSpan(str);
+ span?.setAttribute("unicode.attr", str);
+ span?.updateName(`unicode_${index}_${str}`);
+ span?.end();
+ }, `Unicode string ${index} should not throw`);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: circular reference in error should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("circular-error-test");
+ const circularError: any = new Error("Circular test");
+ circularError.self = circularError; // Create circular reference
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.recordException(circularError);
+ span?.end();
+ }, "Circular reference in error should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "EdgeCase: NaN and Infinity values should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("special-numbers-test");
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("nan.value", NaN as any);
+ span?.setAttribute("infinity.value", Infinity as any);
+ span?.setAttribute("neg.infinity.value", -Infinity as any);
+ span?.end();
+ }, "NaN and Infinity values should not throw");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanHelperUtils.Tests.ts b/AISKU/Tests/Unit/src/SpanHelperUtils.Tests.ts
new file mode 100644
index 000000000..32c89c4d7
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanHelperUtils.Tests.ts
@@ -0,0 +1,992 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import {
+ createDistributedTraceContext,
+ eOTelSpanKind,
+ eOTelSpanStatusCode,
+ IDistributedTraceInit,
+ isReadableSpan,
+ isSpanContextValid,
+ ITelemetryItem,
+ wrapSpanContext
+} from "@microsoft/applicationinsights-core-js";
+
+/**
+ * Comprehensive tests for span helper utility functions
+ *
+ * Tests verify:
+ * - isSpanContextValid: validates span context
+ * - wrapSpanContext: wraps external span contexts
+ * - isReadableSpan: type guard for spans
+ * - createNonRecordingSpan: creates non-recording spans (tested via wrapSpanContext)
+ */
+export class SpanHelperUtilsTests extends AITestClass {
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${SpanHelperUtilsTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "SpanHelperUtilsTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanHelperUtilsTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+ } catch (e) {
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addIsSpanContextValidTests();
+ this.addWrapSpanContextTests();
+ this.addIsReadableSpanTests();
+ this.addHelperIntegrationTests();
+ }
+
+ private addIsSpanContextValidTests(): void {
+ this.testCase({
+ name: "isSpanContextValid: valid span context returns true",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("test-span");
+ const spanContext = span?.spanContext();
+
+ // Act
+ const isValid = isSpanContextValid(spanContext!);
+
+ // Assert
+ Assert.ok(isValid, "Valid span context should return true");
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: valid traceId and spanId returns true",
+ test: () => {
+ // Arrange - create valid context
+ const validContext = createDistributedTraceContext({
+ traceId: "0123456789abcdef0123456789abcdef", // 32 hex chars
+ spanId: "0123456789abcdef", // 16 hex chars
+ traceFlags: 1
+ });
+
+ // Act
+ const isValid = isSpanContextValid(validContext);
+
+ // Assert
+ Assert.ok(isValid, "Context with valid IDs should return true");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: invalid traceId returns false",
+ test: () => {
+ // Arrange - traceId too short (use IDistributedTraceInit directly, not createDistributedTraceContext which validates)
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "0123456789abcdef", // Only 16 chars (should be 32)
+ spanId: "0123456789abcdef",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Context with invalid traceId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: invalid spanId returns false",
+ test: () => {
+ // Arrange - spanId too short (use IDistributedTraceInit directly, not createDistributedTraceContext which validates)
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "0123456789abcdef0123456789abcdef",
+ spanId: "01234567", // Only 8 chars (should be 16)
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Context with invalid spanId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: all zeros traceId returns false",
+ test: () => {
+ // Arrange - all zeros is invalid per spec (use IDistributedTraceInit directly, not createDistributedTraceContext which validates)
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "00000000000000000000000000000000",
+ spanId: "0123456789abcdef",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Context with all-zero traceId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: all zeros spanId returns false",
+ test: () => {
+ // Arrange - all zeros is invalid per spec (use IDistributedTraceInit directly, not createDistributedTraceContext which validates)
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "0123456789abcdef0123456789abcdef",
+ spanId: "0000000000000000",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Context with all-zero spanId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: null context returns false",
+ test: () => {
+ // Act
+ const isValid = isSpanContextValid(null as any);
+
+ // Assert
+ Assert.ok(!isValid, "Null context should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: undefined context returns false",
+ test: () => {
+ // Act
+ const isValid = isSpanContextValid(undefined as any);
+
+ // Assert
+ Assert.ok(!isValid, "Undefined context should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: empty traceId returns false",
+ test: () => {
+ // Arrange - use IDistributedTraceInit directly, not createDistributedTraceContext which validates
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "",
+ spanId: "0123456789abcdef",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Empty traceId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: empty spanId returns false",
+ test: () => {
+ // Arrange - use IDistributedTraceInit directly, not createDistributedTraceContext which validates
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "0123456789abcdef0123456789abcdef",
+ spanId: "",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "Empty spanId should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: non-hex characters in traceId returns false",
+ test: () => {
+ // Arrange - use IDistributedTraceInit directly, not createDistributedTraceContext which validates
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "0123456789abcdefghij456789abcdef", // Contains g-j
+ spanId: "0123456789abcdef",
+ traceFlags: 1
+ };
+
+ // Act
+ const isValid = isSpanContextValid(invalidContext);
+
+ // Assert
+ Assert.ok(!isValid, "TraceId with non-hex chars should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: uppercase hex characters are valid",
+ test: () => {
+ // Arrange
+ const validContext = createDistributedTraceContext({
+ traceId: "0123456789ABCDEF0123456789ABCDEF",
+ spanId: "0123456789ABCDEF",
+ traceFlags: 1
+ });
+
+ // Act
+ const isValid = isSpanContextValid(validContext);
+
+ // Assert
+ Assert.ok(isValid, "Uppercase hex characters should be valid");
+ }
+ });
+
+ this.testCase({
+ name: "isSpanContextValid: mixed case hex characters are valid",
+ test: () => {
+ // Arrange
+ const validContext = createDistributedTraceContext({
+ traceId: "0123456789AbCdEf0123456789AbCdEf",
+ spanId: "0123456789AbCdEf",
+ traceFlags: 1
+ });
+
+ // Act
+ const isValid = isSpanContextValid(validContext);
+
+ // Assert
+ Assert.ok(isValid, "Mixed case hex characters should be valid");
+ }
+ });
+ }
+
+ private addWrapSpanContextTests(): void {
+ this.testCase({
+ name: "wrapSpanContext: creates non-recording span from context",
+ test: () => {
+ // Arrange
+ const originalSpan = this._ai.startSpan("original-span");
+ const spanContext = originalSpan?.spanContext();
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext!);
+
+ // Assert
+ Assert.ok(wrappedSpan, "Wrapped span should be created");
+ Assert.ok(!wrappedSpan.isRecording(), "Wrapped span should not be recording");
+ Assert.equal(wrappedSpan.spanContext().traceId, spanContext?.traceId, "TraceId should match");
+ Assert.equal(wrappedSpan.spanContext().spanId, spanContext?.spanId, "SpanId should match");
+
+ // Cleanup
+ originalSpan?.end();
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: wrapped span name includes spanId",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "0123456789abcdef0123456789abcdef",
+ spanId: "0123456789abcdef",
+ traceFlags: 1
+ });
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Assert
+ Assert.ok(wrappedSpan.name.includes(spanContext.spanId),
+ "Wrapped span name should include spanId");
+ Assert.ok(wrappedSpan.name.includes("wrapped"),
+ "Wrapped span name should indicate it's wrapped");
+
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: wrapped span does not generate telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spanContext = createDistributedTraceContext({
+ traceId: "abcdef0123456789abcdef0123456789",
+ spanId: "abcdef0123456789",
+ traceFlags: 1
+ });
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+ wrappedSpan.setAttribute("test", "value");
+ wrappedSpan.setStatus({ code: eOTelSpanStatusCode.OK });
+ wrappedSpan.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 0,
+ "Wrapped span should not generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: wrapped span kind is INTERNAL",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "fedcba9876543210fedcba9876543210",
+ spanId: "fedcba9876543210",
+ traceFlags: 1
+ });
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Assert
+ Assert.equal(wrappedSpan.kind, eOTelSpanKind.INTERNAL,
+ "Wrapped span should have INTERNAL kind");
+
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: can use wrapped span as parent",
+ test: () => {
+ // Arrange
+ const parentContext = createDistributedTraceContext({
+ traceId: "1234567890abcdef1234567890abcdef",
+ spanId: "1234567890abcdef",
+ traceFlags: 1
+ });
+ const wrappedParent = wrapSpanContext(this._ai.otelApi, parentContext);
+
+ // Act - create child with wrapped parent
+ const childSpan = this._ai.startSpan("child-span", {
+ kind: eOTelSpanKind.CLIENT
+ }, wrappedParent.spanContext());
+
+ // Assert
+ Assert.ok(childSpan, "Child span should be created");
+ Assert.equal(childSpan.spanContext().traceId, parentContext.traceId,
+ "Child should have same traceId as wrapped parent");
+
+ // Cleanup
+ childSpan?.end();
+ wrappedParent.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: wrapped span supports all span operations",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "aabbccddeeff00112233445566778899",
+ spanId: "aabbccddeeff0011",
+ traceFlags: 1
+ });
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Act - perform various operations
+ wrappedSpan.setAttribute("key1", "value1");
+ wrappedSpan.setAttributes({
+ "key2": 123,
+ "key3": true
+ });
+ wrappedSpan.updateName("new-name");
+ wrappedSpan.setStatus({ code: eOTelSpanStatusCode.OK, message: "Success" });
+ wrappedSpan.recordException(new Error("Test error"));
+
+ // Assert - operations should not throw
+ Assert.ok(true, "All operations should complete without error");
+ Assert.equal(wrappedSpan.name, "new-name", "Name should be updated");
+
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: preserves traceFlags if present",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "11112222333344445555666677778888",
+ spanId: "1111222233334444",
+ traceFlags: 1 // Sampled
+ });
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Assert
+ Assert.equal(wrappedSpan.spanContext().traceFlags, 1,
+ "TraceFlags should be preserved");
+
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: multiple wrapped spans from same context are independent",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "99887766554433221100ffeeddccbbaa",
+ spanId: "9988776655443322",
+ traceFlags: 1
+ });
+
+ // Act
+ const wrapped1 = wrapSpanContext(this._ai.otelApi, spanContext);
+ const wrapped2 = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Assert - both wrap same context but are different span objects
+ Assert.notEqual(wrapped1, wrapped2, "Should create different span objects");
+ Assert.equal(wrapped1.spanContext().traceId, wrapped2.spanContext().traceId,
+ "Both should have same traceId");
+ Assert.equal(wrapped1.spanContext().spanId, wrapped2.spanContext().spanId,
+ "Both should have same spanId");
+
+ // Operations on one should not affect the other
+ wrapped1.updateName("wrapped-1");
+ wrapped2.updateName("wrapped-2");
+ Assert.equal(wrapped1.name, "wrapped-1", "First span name");
+ Assert.equal(wrapped2.name, "wrapped-2", "Second span name");
+
+ wrapped1.end();
+ wrapped2.end();
+ }
+ });
+
+ this.testCase({
+ name: "wrapSpanContext: can wrap context from external system",
+ test: () => {
+ // Arrange - simulate receiving context from external system (e.g., HTTP header)
+ const externalContext = createDistributedTraceContext({
+ traceId: "00112233445566778899aabbccddeeff",
+ spanId: "0011223344556677",
+ traceFlags: 1
+ });
+
+ // Act
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, externalContext);
+
+ // Create child span to continue the trace
+ const childSpan = this._ai.startSpan("continue-external-trace", {
+ kind: eOTelSpanKind.SERVER
+ }, wrappedSpan.spanContext());
+
+ // Assert
+ Assert.equal(childSpan?.spanContext().traceId, externalContext.traceId,
+ "Should continue external trace");
+ Assert.notEqual(childSpan?.spanContext().spanId, externalContext.spanId,
+ "Should have new spanId");
+
+ // Cleanup
+ childSpan?.end();
+ wrappedSpan.end();
+ }
+ });
+ }
+
+ private addIsReadableSpanTests(): void {
+ this.testCase({
+ name: "isReadableSpan: valid span returns true",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("test-span");
+
+ // Act
+ const isValid = isReadableSpan(span);
+
+ // Assert
+ Assert.ok(isValid, "Valid span should return true");
+
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: null returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan(null);
+
+ // Assert
+ Assert.ok(!isValid, "Null should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: undefined returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan(undefined);
+
+ // Assert
+ Assert.ok(!isValid, "Undefined should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: plain object returns false",
+ test: () => {
+ // Arrange
+ const notASpan = {
+ name: "fake-span",
+ kind: eOTelSpanKind.INTERNAL
+ };
+
+ // Act
+ const isValid = isReadableSpan(notASpan);
+
+ // Assert
+ Assert.ok(!isValid, "Plain object should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: object with partial span interface returns false",
+ test: () => {
+ // Arrange - object with some but not all required properties
+ const partialSpan = {
+ name: "partial",
+ kind: eOTelSpanKind.CLIENT,
+ spanContext: () => ({ traceId: "123", spanId: "456" }),
+ // Missing: duration, ended, startTime, endTime, etc.
+ };
+
+ // Act
+ const isValid = isReadableSpan(partialSpan);
+
+ // Assert
+ Assert.ok(!isValid, "Partial span interface should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: recording span returns true",
+ test: () => {
+ // Arrange
+ const recordingSpan = this._ai.startSpan("recording", { recording: true });
+
+ // Act
+ const isValid = isReadableSpan(recordingSpan);
+
+ // Assert
+ Assert.ok(isValid, "Recording span should return true");
+
+ recordingSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: non-recording span returns true",
+ test: () => {
+ // Arrange
+ const nonRecordingSpan = this._ai.startSpan("non-recording", { recording: false });
+
+ // Act
+ const isValid = isReadableSpan(nonRecordingSpan);
+
+ // Assert
+ Assert.ok(isValid, "Non-recording span should return true");
+
+ nonRecordingSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: wrapped span context returns true",
+ test: () => {
+ // Arrange
+ const spanContext = createDistributedTraceContext({
+ traceId: "aabbccdd00112233aabbccdd00112233",
+ spanId: "aabbccdd00112233",
+ traceFlags: 1
+ });
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, spanContext);
+
+ // Act
+ const isValid = isReadableSpan(wrappedSpan);
+
+ // Assert
+ Assert.ok(isValid, "Wrapped span should return true");
+
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: ended span returns true",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-span");
+ span?.end();
+
+ // Act
+ const isValid = isReadableSpan(span);
+
+ // Assert
+ Assert.ok(isValid, "Ended span should still return true");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: span with all kinds returns true",
+ test: () => {
+ // Arrange & Act & Assert
+ const internalSpan = this._ai.startSpan("internal", { kind: eOTelSpanKind.INTERNAL });
+ Assert.ok(isReadableSpan(internalSpan), "INTERNAL span should be valid");
+ internalSpan?.end();
+
+ const clientSpan = this._ai.startSpan("client", { kind: eOTelSpanKind.CLIENT });
+ Assert.ok(isReadableSpan(clientSpan), "CLIENT span should be valid");
+ clientSpan?.end();
+
+ const serverSpan = this._ai.startSpan("server", { kind: eOTelSpanKind.SERVER });
+ Assert.ok(isReadableSpan(serverSpan), "SERVER span should be valid");
+ serverSpan?.end();
+
+ const producerSpan = this._ai.startSpan("producer", { kind: eOTelSpanKind.PRODUCER });
+ Assert.ok(isReadableSpan(producerSpan), "PRODUCER span should be valid");
+ producerSpan?.end();
+
+ const consumerSpan = this._ai.startSpan("consumer", { kind: eOTelSpanKind.CONSUMER });
+ Assert.ok(isReadableSpan(consumerSpan), "CONSUMER span should be valid");
+ consumerSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: string returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan("not a span");
+
+ // Assert
+ Assert.ok(!isValid, "String should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: number returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan(12345);
+
+ // Assert
+ Assert.ok(!isValid, "Number should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: array returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan([]);
+
+ // Assert
+ Assert.ok(!isValid, "Array should return false");
+ }
+ });
+
+ this.testCase({
+ name: "isReadableSpan: function returns false",
+ test: () => {
+ // Act
+ const isValid = isReadableSpan(() => {});
+
+ // Assert
+ Assert.ok(!isValid, "Function should return false");
+ }
+ });
+ }
+
+ private addHelperIntegrationTests(): void {
+ this.testCase({
+ name: "Integration: validate context before wrapping",
+ test: () => {
+ // Arrange - good practice to validate before wrapping
+ const validContext: IDistributedTraceInit = {
+ traceId: "aaaabbbbccccddddeeeeffffaaaabbbb",
+ spanId: "aaaabbbbccccdddd",
+ traceFlags: 1
+ };
+ const invalidContext: IDistributedTraceInit = {
+ traceId: "invalid",
+ spanId: "also-bad",
+ traceFlags: 1
+ };
+
+ // Act & Assert - validate before wrapping
+ Assert.ok(isSpanContextValid(validContext), "Valid context should pass validation");
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, validContext);
+ Assert.ok(wrappedSpan, "Should wrap valid context");
+ Assert.ok(isReadableSpan(wrappedSpan), "Wrapped span should be readable");
+ wrappedSpan.end();
+
+ // Don't wrap invalid context
+ Assert.ok(!isSpanContextValid(invalidContext),
+ "Should detect invalid context before wrapping");
+ }
+ });
+
+ this.testCase({
+ name: "Integration: type-safe span handling with isReadableSpan",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("type-safe");
+ const maybeSpan: any = span;
+
+ // Act - type guard allows safe access
+ if (isReadableSpan(maybeSpan)) {
+ // TypeScript knows this is IReadableSpan now
+ const context = maybeSpan.spanContext();
+ maybeSpan.setAttribute("safe", "access");
+ maybeSpan.end();
+
+ // Assert
+ Assert.ok(context.traceId, "Can safely access span properties");
+ } else {
+ Assert.ok(false, "Span should be readable");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "Integration: wrap external trace and continue locally",
+ test: () => {
+ // Arrange - simulate receiving trace context from external service
+ const externalContext = createDistributedTraceContext({
+ traceId: "1234567890abcdef1234567890abcdef",
+ spanId: "1234567890abcdef",
+ traceFlags: 1
+ });
+
+ // Act - validate and wrap
+ Assert.ok(isSpanContextValid(externalContext),
+ "External context should be valid");
+
+ const wrappedExternal = wrapSpanContext(
+ this._ai.otelApi,
+ externalContext
+ );
+ Assert.ok(isReadableSpan(wrappedExternal),
+ "Wrapped external should be readable");
+
+ // Continue trace with local spans
+ const localSpan1 = this._ai.startSpan("local-processing", {
+ kind: eOTelSpanKind.SERVER
+ }, wrappedExternal.spanContext());
+
+ const localSpan2 = this._ai.startSpan("database-call", {
+ kind: eOTelSpanKind.CLIENT
+ }, localSpan1?.spanContext());
+
+ // Assert - trace continuity
+ Assert.equal(localSpan1?.spanContext().traceId, externalContext.traceId,
+ "Local span should continue external trace");
+ Assert.equal(localSpan2?.spanContext().traceId, externalContext.traceId,
+ "Nested span should continue external trace");
+ Assert.notEqual(localSpan2?.spanContext().spanId, externalContext.spanId,
+ "Should have new span IDs");
+
+ // Cleanup
+ localSpan2?.end();
+ localSpan1?.end();
+ wrappedExternal.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: helper functions work with all span kinds",
+ test: () => {
+ // Test each span kind
+ const kinds = [
+ eOTelSpanKind.INTERNAL,
+ eOTelSpanKind.CLIENT,
+ eOTelSpanKind.SERVER,
+ eOTelSpanKind.PRODUCER,
+ eOTelSpanKind.CONSUMER
+ ];
+
+ for (const kind of kinds) {
+ // Create span
+ const span = this._ai.startSpan(`span-kind-${kind}`, { kind });
+ Assert.ok(span, `Span with kind ${kind} should be created`);
+
+ // Verify with isReadableSpan
+ Assert.ok(isReadableSpan(span), `Span kind ${kind} should be readable`);
+
+ // Get and validate context
+ const context = span?.spanContext();
+ Assert.ok(isSpanContextValid(context!),
+ `Span kind ${kind} should have valid context`);
+
+ // Wrap the context
+ const wrapped = wrapSpanContext(this._ai.otelApi, context!);
+ Assert.ok(isReadableSpan(wrapped),
+ `Wrapped span from kind ${kind} should be readable`);
+
+ // Cleanup
+ span?.end();
+ wrapped.end();
+ }
+
+ Assert.ok(true, "All span kinds tested successfully");
+ }
+ });
+
+ this.testCase({
+ name: "Integration: helpers work after span lifecycle",
+ test: () => {
+ // Arrange - create and end span
+ const span = this._ai.startSpan("lifecycle-test");
+ const context = span?.spanContext();
+ span?.end();
+
+ // Act & Assert - helpers should still work with ended span
+ Assert.ok(isReadableSpan(span),
+ "isReadableSpan should work with ended span");
+ Assert.ok(isSpanContextValid(context!),
+ "isSpanContextValid should work with context from ended span");
+
+ const wrapped = wrapSpanContext(this._ai.otelApi, context!);
+ Assert.ok(isReadableSpan(wrapped),
+ "Can wrap context from ended span");
+
+ wrapped.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: defensive programming with helpers",
+ test: () => {
+ // Arrange - potentially problematic inputs
+ const nullValue: any = null;
+ const undefinedValue: any = undefined;
+ const emptyObject: any = {};
+ const wrongType: any = "not a span";
+
+ // Act & Assert - helpers should handle gracefully
+ Assert.ok(!isReadableSpan(nullValue), "Handle null");
+ Assert.ok(!isReadableSpan(undefinedValue), "Handle undefined");
+ Assert.ok(!isReadableSpan(emptyObject), "Handle empty object");
+ Assert.ok(!isReadableSpan(wrongType), "Handle wrong type");
+
+ Assert.ok(!isSpanContextValid(nullValue), "Validate null context");
+ Assert.ok(!isSpanContextValid(undefinedValue), "Validate undefined context");
+ Assert.ok(!isSpanContextValid(emptyObject), "Validate empty context");
+ Assert.ok(!isSpanContextValid(wrongType), "Validate wrong type context");
+ }
+ });
+
+ this.testCase({
+ name: "Integration: wrap and use as active span",
+ test: () => {
+ // Arrange
+ const externalContext = createDistributedTraceContext({
+ traceId: "activespan123456789012345678901234",
+ spanId: "activespan123456",
+ traceFlags: 1
+ });
+
+ // Act - wrap and set as active
+ const wrappedSpan = wrapSpanContext(this._ai.otelApi, externalContext);
+ const scope = this._ai.core.setActiveSpan(wrappedSpan);
+
+ // Create child that should automatically get wrapped span as parent
+ const childSpan = this._ai.startSpan("auto-child", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+
+ // Assert
+ Assert.equal(childSpan?.spanContext().traceId, externalContext.traceId,
+ "Child should inherit traceId from active wrapped span");
+
+ // Cleanup
+ childSpan?.end();
+ scope?.restore();
+ wrappedSpan.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: validation chain for incoming distributed trace",
+ test: () => {
+ // Simulate complete flow of receiving and processing distributed trace
+
+ // Step 1: Receive trace context (e.g., from HTTP headers)
+ const receivedContext = createDistributedTraceContext({
+ traceId: "abcdef0123456789abcdef0123456789",
+ spanId: "abcdef0123456789",
+ traceFlags: 1
+ });
+
+ // Step 2: Validate received context
+ Assert.ok(isSpanContextValid(receivedContext), "Received trace context should be valid");
+
+ // Step 3: Wrap context to create local span representation
+ const remoteSpan = wrapSpanContext(this._ai.otelApi, receivedContext);
+ Assert.ok(isReadableSpan(remoteSpan), "Failed to create readable span");
+
+ // Step 4: Create local server span as child
+ const serverSpan = this._ai.startSpan("handle-request", {
+ kind: eOTelSpanKind.SERVER
+ }, remoteSpan.spanContext());
+
+ Assert.ok(isReadableSpan(serverSpan), "Server span should be readable");
+
+ // Step 5: Verify trace continuity
+ const serverContext = serverSpan?.spanContext();
+ Assert.ok(isSpanContextValid(serverContext!),
+ "Server span should have valid context");
+ Assert.equal(serverContext?.traceId, receivedContext.traceId,
+ "Trace ID should be preserved across process boundary");
+
+ // Step 6: Complete request handling
+ serverSpan?.setAttribute("http.status_code", 200);
+ serverSpan?.setStatus({ code: eOTelSpanStatusCode.OK });
+ serverSpan?.end();
+ remoteSpan.end();
+
+ Assert.ok(true, "Complete distributed trace flow validated");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanLifeCycle.Tests.ts b/AISKU/Tests/Unit/src/SpanLifeCycle.Tests.ts
new file mode 100644
index 000000000..34cc169b5
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanLifeCycle.Tests.ts
@@ -0,0 +1,655 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { eOTelSpanStatusCode, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+
+export class SpanLifeCycleTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${SpanLifeCycleTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "SpanLifeCycleTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanLifeCycleTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addDoubleEndTests();
+ this.addOperationsOnEndedSpansTests();
+ this.addEndedPropertyTests();
+ this.addIsRecordingAfterEndTests();
+ this.addEndTimeTests();
+ }
+
+ private addDoubleEndTests(): void {
+ this.testCase({
+ name: "DoubleEnd: calling end() twice should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("double-end-test");
+
+ // Act & Assert - First end should succeed
+ Assert.doesNotThrow(() => {
+ span?.end();
+ }, "First end() should not throw");
+
+ // Second end should not throw but should be no-op
+ Assert.doesNotThrow(() => {
+ span?.end();
+ }, "Second end() should not throw");
+ }
+ });
+
+ this.testCase({
+ name: "DoubleEnd: second end() should be no-op",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("double-end-noop");
+ this._trackCalls = [];
+
+ // Act - End twice
+ span?.end();
+ const trackCountAfterFirst = this._trackCalls.length;
+
+ span?.end();
+ const trackCountAfterSecond = this._trackCalls.length;
+
+ // Assert - Second end should not generate additional telemetry
+ Assert.equal(trackCountAfterSecond, trackCountAfterFirst,
+ "Second end() should not generate additional telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "DoubleEnd: ended property remains true after second end",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("double-end-property");
+
+ // Act
+ span?.end();
+ const endedAfterFirst = span?.ended;
+
+ span?.end();
+ const endedAfterSecond = span?.ended;
+
+ // Assert
+ Assert.ok(endedAfterFirst, "Span should be ended after first end()");
+ Assert.ok(endedAfterSecond, "Span should remain ended after second end()");
+ }
+ });
+
+ this.testCase({
+ name: "DoubleEnd: multiple end() calls are safe",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("multiple-end-test");
+
+ // Act & Assert - Multiple ends should all be safe
+ Assert.doesNotThrow(() => {
+ for (let i = 0; i < 10; i++) {
+ span?.end();
+ }
+ }, "Multiple end() calls should not throw");
+
+ Assert.ok(span?.ended, "Span should be marked as ended");
+ }
+ });
+
+ this.testCase({
+ name: "DoubleEnd: end with different times only uses first",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("double-end-time");
+
+ // Act - End with specific time
+ const firstEndTime = Date.now();
+ span?.end(firstEndTime);
+ const capturedEndTime1 = span?.endTime;
+
+ // Try to end again with different time
+ const secondEndTime = Date.now() + 1000;
+ span?.end(secondEndTime);
+ const capturedEndTime2 = span?.endTime;
+
+ // Assert - End time should not change
+ Assert.deepEqual(capturedEndTime1, capturedEndTime2,
+ "End time should not change on second end()");
+ }
+ });
+ }
+
+ private addOperationsOnEndedSpansTests(): void {
+ this.testCase({
+ name: "EndedSpan: setAttribute on ended span should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setAttribute");
+ span?.end();
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("after.end", "value");
+ }, "setAttribute should not throw on ended span");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: setAttribute on ended span should be no-op",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setAttribute-noop");
+ span?.setAttribute("before.end", "initialValue");
+
+ const attributesBeforeEnd = span?.attributes;
+ span?.end();
+
+ // Act
+ span?.setAttribute("after.end", "newValue");
+ span?.setAttribute("before.end", "modifiedValue");
+
+ // Assert
+ const attributesAfterEnd = span?.attributes;
+ Assert.ok(!attributesAfterEnd["after.end"],
+ "New attribute should not be added after end");
+ Assert.equal(attributesAfterEnd["before.end"], "initialValue",
+ "Existing attribute should not be modified after end");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: setAttributes on ended span should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setAttributes");
+ span?.end();
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setAttributes({
+ "attr1": "value1",
+ "attr2": "value2"
+ });
+ }, "setAttributes should not throw on ended span");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: setAttributes on ended span should be no-op",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setAttributes-noop");
+ span?.setAttributes({ "initial": "value" });
+ span?.end();
+
+ // Act
+ span?.setAttributes({
+ "after.end.1": "value1",
+ "after.end.2": "value2"
+ });
+
+ // Assert
+ const attributes = span?.attributes;
+ Assert.ok(!attributes["after.end.1"],
+ "Attributes should not be added after end");
+ Assert.ok(!attributes["after.end.2"],
+ "Attributes should not be added after end");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: setStatus on ended span should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setStatus");
+ span?.end();
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Error after end"
+ });
+ }, "setStatus should not throw on ended span");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: setStatus on ended span should be no-op",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-setStatus-noop");
+ span?.setStatus({
+ code: eOTelSpanStatusCode.OK,
+ message: "Initial status"
+ });
+
+ const statusBeforeEnd = span?.status;
+ span?.end();
+
+ // Act
+ span?.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Modified after end"
+ });
+
+ // Assert
+ const statusAfterEnd = span?.status;
+ Assert.equal(statusAfterEnd.code, statusBeforeEnd?.code,
+ "Status code should not change after end");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: updateName on ended span should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("original-name");
+ span?.end();
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.updateName("new-name-after-end");
+ }, "updateName should not throw on ended span");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: updateName on ended span should be no-op",
+ test: () => {
+ // Arrange
+ const originalName = "original-name-noop";
+ const span = this._ai.startSpan(originalName);
+ span?.end();
+
+ // Act
+ span?.updateName("modified-name");
+
+ // Assert
+ Assert.equal(span?.name, originalName,
+ "Span name should not change after end");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: recordException on ended span should not throw",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-recordException");
+ span?.end();
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ span?.recordException(new Error("Exception after end"));
+ }, "recordException should not throw on ended span");
+ }
+ });
+
+ this.testCase({
+ name: "EndedSpan: multiple operations on ended span should all be safe",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-multiple-ops");
+ span?.end();
+
+ // Act & Assert - All operations should be safe
+ Assert.doesNotThrow(() => {
+ span?.setAttribute("key", "value");
+ span?.setAttributes({ "key1": "val1", "key2": "val2" });
+ span?.setStatus({ code: eOTelSpanStatusCode.ERROR });
+ span?.updateName("new-name");
+ span?.recordException(new Error("test"));
+ span?.end(); // Try to end again
+ }, "Multiple operations on ended span should not throw");
+ }
+ });
+ }
+
+ private addEndedPropertyTests(): void {
+ this.testCase({
+ name: "EndedProperty: span should not be ended initially",
+ test: () => {
+ // Arrange & Act
+ const span = this._ai.startSpan("initial-not-ended");
+
+ // Assert
+ Assert.ok(!span?.ended, "Span should not be ended initially");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "EndedProperty: span should be ended after end() call",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-after-call");
+
+ // Act
+ span?.end();
+
+ // Assert
+ Assert.ok(span?.ended, "Span should be ended after end() call");
+ }
+ });
+
+ this.testCase({
+ name: "EndedProperty: ended property is read-only",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("readonly-ended") as any;
+
+ // Act - Try to modify ended property
+ const canModify = () => {
+ try {
+ span.ended = true;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ // Assert
+ Assert.ok(!span.ended, "Should start not ended");
+ // Property should be read-only (or modification has no effect)
+ canModify();
+ Assert.ok(!span.ended, "Manual modification should not affect ended state");
+
+ // Cleanup
+ span.end();
+ }
+ });
+
+ this.testCase({
+ name: "EndedProperty: ended state persists across property reads",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("persistent-ended");
+ span?.end();
+
+ // Act - Read ended property multiple times
+ const ended1 = span?.ended;
+ const ended2 = span?.ended;
+ const ended3 = span?.ended;
+
+ // Assert
+ Assert.ok(ended1, "First read should show ended");
+ Assert.ok(ended2, "Second read should show ended");
+ Assert.ok(ended3, "Third read should show ended");
+ }
+ });
+
+ this.testCase({
+ name: "EndedProperty: recording and non-recording spans both have ended property",
+ test: () => {
+ // Arrange
+ const recordingSpan = this._ai.startSpan("recording", { recording: true });
+ const nonRecordingSpan = this._ai.startSpan("non-recording", { recording: false });
+
+ // Act
+ recordingSpan?.end();
+ nonRecordingSpan?.end();
+
+ // Assert
+ Assert.ok(recordingSpan?.ended, "Recording span should be ended");
+ Assert.ok(nonRecordingSpan?.ended, "Non-recording span should be ended");
+ }
+ });
+ }
+
+ private addIsRecordingAfterEndTests(): void {
+ this.testCase({
+ name: "IsRecording: isRecording() returns false after end()",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("recording-test");
+ const isRecordingBefore = span?.isRecording();
+
+ // Act
+ span?.end();
+ const isRecordingAfter = span?.isRecording();
+
+ // Assert
+ Assert.ok(isRecordingBefore, "Span should be recording before end");
+ Assert.ok(!isRecordingAfter, "Span should not be recording after end");
+ }
+ });
+
+ this.testCase({
+ name: "IsRecording: non-recording span stays non-recording after end",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("non-recording-test", { recording: false });
+ const isRecordingBefore = span?.isRecording();
+
+ // Act
+ span?.end();
+ const isRecordingAfter = span?.isRecording();
+
+ // Assert
+ Assert.ok(!isRecordingBefore, "Non-recording span should not be recording before end");
+ Assert.ok(!isRecordingAfter, "Non-recording span should not be recording after end");
+ }
+ });
+
+ this.testCase({
+ name: "IsRecording: isRecording() consistent with ended state",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("recording-consistency");
+
+ // Assert initial state
+ Assert.ok(span?.isRecording(), "Should be recording when not ended");
+ Assert.ok(!span?.ended, "Should not be ended initially");
+
+ // Act
+ span?.end();
+
+ // Assert final state
+ Assert.ok(!span?.isRecording(), "Should not be recording when ended");
+ Assert.ok(span?.ended, "Should be ended after end()");
+ }
+ });
+
+ this.testCase({
+ name: "IsRecording: multiple isRecording() calls after end return consistent value",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("recording-multiple-calls");
+ span?.end();
+
+ // Act
+ const check1 = span?.isRecording();
+ const check2 = span?.isRecording();
+ const check3 = span?.isRecording();
+
+ // Assert
+ Assert.ok(!check1, "First check should return false");
+ Assert.ok(!check2, "Second check should return false");
+ Assert.ok(!check3, "Third check should return false");
+ }
+ });
+ }
+
+ private addEndTimeTests(): void {
+ this.testCase({
+ name: "EndTime: endTime is undefined before end()",
+ test: () => {
+ // Arrange & Act
+ const span = this._ai.startSpan("endtime-undefined");
+ const endTime = span?.endTime;
+
+ // Assert
+ Assert.ok(endTime === undefined || endTime === null,
+ "endTime should be undefined/null before end()");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: endTime is set after end()",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("endtime-set");
+
+ // Act
+ span?.end();
+ const endTime = span?.endTime;
+
+ // Assert
+ Assert.ok(endTime !== undefined && endTime !== null,
+ "endTime should be set after end()");
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: endTime is after startTime",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("endtime-after-start");
+
+ // Act
+ span?.end();
+
+ // Assert
+ const startTime = span?.startTime;
+ const endTime = span?.endTime;
+
+ if (startTime && endTime) {
+ // Compare HrTime [seconds, nanoseconds]
+ const startMs = startTime[0] * 1000 + startTime[1] / 1000000;
+ const endMs = endTime[0] * 1000 + endTime[1] / 1000000;
+
+ Assert.ok(endMs >= startMs,
+ "endTime should be after or equal to startTime");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: custom endTime is respected",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("endtime-custom");
+ const customEndTime = Date.now();
+
+ // Act
+ span?.end(customEndTime);
+ const actualEndTime = span?.endTime;
+
+ // Assert
+ if (actualEndTime) {
+ const actualMs = actualEndTime[0] * 1000 + actualEndTime[1] / 1000000;
+ const diff = Math.abs(actualMs - customEndTime);
+
+ Assert.ok(diff < 10, // Allow 10ms difference for conversion
+ "Custom endTime should be approximately respected");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: duration is calculated from startTime to endTime",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("duration-calculation");
+
+ // Act - Add small delay
+ const startTime = Date.now();
+ for (let i = 0; i < 1000; i++) {
+ // Small busy loop
+ }
+ span?.end();
+
+ // Assert
+ const duration = span?.duration;
+ if (duration) {
+ const durationMs = duration[0] * 1000 + duration[1] / 1000000;
+ Assert.ok(durationMs >= 0, "Duration should be non-negative");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: endTime does not change after span is ended",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("endtime-immutable");
+
+ // Act
+ span?.end();
+ const endTime1 = span?.endTime;
+
+ // Try to end again (should be no-op)
+ span?.end();
+ const endTime2 = span?.endTime;
+
+ // Assert
+ Assert.deepEqual(endTime1, endTime2,
+ "endTime should not change after first end()");
+ }
+ });
+
+ this.testCase({
+ name: "EndTime: negative duration is handled gracefully",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("negative-duration");
+
+ // Act - End with time before start
+ const futureTime = Date.now() + 10000;
+ span?.end();
+
+ // Try to set past end time after span started
+ // (Note: SDK should handle this internally and prevent negative duration)
+
+ // Assert
+ const duration = span?.duration;
+ if (duration) {
+ const durationMs = duration[0] * 1000 + duration[1] / 1000000;
+ Assert.ok(durationMs >= 0,
+ "Duration should never be negative (SDK should handle this)");
+ }
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanPluginIntegration.Tests.ts b/AISKU/Tests/Unit/src/SpanPluginIntegration.Tests.ts
new file mode 100644
index 000000000..35b5fb077
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanPluginIntegration.Tests.ts
@@ -0,0 +1,1020 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import { eOTelSpanKind, eOTelSpanStatusCode, isTracingSuppressed, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+import { setBypassLazyCache } from "@nevware21/ts-utils";
+
+/**
+ * Integration Tests for Span APIs with Properties Plugin and Analytics Plugin
+ *
+ * Tests verify that span telemetry correctly integrates with:
+ * - PropertiesPlugin: session, user, device, application context
+ * - AnalyticsPlugin: telemetry creation, dependency tracking, page views
+ * - Telemetry Initializers: custom property injection
+ * - SDK configuration: sampling, disabled tracking, etc.
+ */
+export class SpanPluginIntegrationTests extends AITestClass {
+ private _ai!: ApplicationInsights;
+
+ constructor(testName?: string) {
+ super(testName || "SpanPluginIntegrationTests");
+ }
+
+ public testInitialize() {
+ try {
+ setBypassLazyCache(true);
+ this.useFakeServer = true;
+
+ this._ai = new ApplicationInsights({
+ config: {
+ instrumentationKey: "test-ikey-123",
+ disableInstrumentationKeyValidation: true,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ disableFetchTracking: false,
+ enableAutoRouteTracking: false,
+ disableExceptionTracking: false,
+ maxBatchInterval: 100,
+ enableDebug: false,
+ extensionConfig: {
+ ["AppInsightsPropertiesPlugin"]: {
+ accountId: "test-account-id"
+ }
+ }
+ }
+ });
+
+ this._ai.loadAppInsights();
+ } catch (e) {
+ Assert.ok(false, "Failed to initialize tests: " + e);
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ setBypassLazyCache(false);
+ }
+
+ public registerTests() {
+ this.addPropertiesPluginIntegrationTests();
+ this.addAnalyticsPluginIntegrationTests();
+ this.addTelemetryInitializerTests();
+ this.addSessionContextTests();
+ this.addUserContextTests();
+ this.addDeviceContextTests();
+ this.addDistributedTraceContextTests();
+ this.addSamplingIntegrationTests();
+ this.addConfigurationIntegrationTests();
+ }
+
+ private addPropertiesPluginIntegrationTests(): void {
+ this.testCase({
+ name: "PropertiesPlugin: span telemetry includes session context",
+ useFakeTimers: true,
+ useFakeServer: true,
+ test: () => {
+ const span = this._ai.startSpan("test-operation", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+
+ Assert.equal(false, isTracingSuppressed(span), "Span should not be suppressed");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(1, sentItems.length, "Telemetry should be sent");
+
+ const payload = sentItems[0];
+ Assert.ok(payload, "Payload should exist");
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // Session ID is sent in tags with key "ai.session.id"
+ const sessionId = payload.tags["ai.session.id"];
+ Assert.ok(sessionId, "Session ID should be in tags");
+ Assert.ok(sessionId.length > 0, "Session ID should not be empty");
+ }
+ });
+
+ this.testCase({
+ name: "PropertiesPlugin: span telemetry includes user context",
+ useFakeTimers: true,
+ test: () => {
+ // Set user context before creating span
+ this._ai.context.user.authenticatedId = "test-auth-user-123";
+ this._ai.context.user.accountId = "test-account-456";
+
+ const span = this._ai.startSpan("user-operation", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.setAttribute("custom.prop", "value");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(1, sentItems.length, "Telemetry should be sent");
+
+ const payload = sentItems[0];
+ Assert.ok(payload, "Payload should exist");
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // User auth ID is sent in tags with key "ai.user.authUserId"
+ const authUserId = payload.tags["ai.user.authUserId"];
+ Assert.equal(authUserId, "test-auth-user-123", "Authenticated ID should match");
+
+ // Account ID is sent in tags with key "ai.user.accountId"
+ const accountId = payload.tags["ai.user.accountId"];
+ Assert.equal(accountId, "test-account-456", "Account ID should be in tags");
+ }
+ });
+
+ this.testCase({
+ name: "PropertiesPlugin: span telemetry includes device context",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("device-operation", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(1, sentItems.length, "Telemetry should be sent");
+ const payload = sentItems[0];
+ Assert.ok(payload, "Payload should exist");
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // Device info is sent in tags with keys "ai.device.type" and "ai.device.id"
+ const deviceType = payload.tags["ai.device.type"];
+ Assert.equal(deviceType, "Browser", "Device type should be Browser");
+
+ const deviceId = payload.tags["ai.device.id"];
+ Assert.equal(deviceId, "browser", "Device ID should be browser");
+ }
+ });
+
+ this.testCase({
+ name: "PropertiesPlugin: span telemetry includes SDK version from internal context",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("sdk-version-check", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(1, sentItems.length, "Telemetry should be sent");
+ const payload = sentItems[0];
+ Assert.ok(payload, "Payload should exist");
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // SDK version is sent in tags with key "ai.internal.sdkVersion"
+ const sdkVersion = payload.tags["ai.internal.sdkVersion"];
+ Assert.ok(sdkVersion, "SDK version should exist");
+ Assert.ok(sdkVersion.indexOf("javascript") >= 0 || sdkVersion.indexOf("ext1") >= 0,
+ "SDK version should contain javascript or extension prefix");
+ }
+ });
+
+ this.testCase({
+ name: "PropertiesPlugin: web context applied to span telemetry",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("web-context-operation", {
+ kind: eOTelSpanKind.SERVER
+ });
+ Assert.ok(span, "Span should be created");
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(1, sentItems.length, "Telemetry should be sent");
+
+ const payload = sentItems[0];
+ Assert.ok(payload, "Payload should exist");
+
+ // Web context info like browser is sent in data section or tags
+ // Just verify the payload was sent successfully with telemetry
+ Assert.ok(payload.data, "Payload should have data section");
+ }
+ });
+ }
+
+ private addAnalyticsPluginIntegrationTests(): void {
+ this.testCase({
+ name: "AnalyticsPlugin: CLIENT span creates RemoteDependencyData",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("http-request", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://api.example.com/data",
+ "http.status_code": 200
+ }
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ span.setStatus({ code: eOTelSpanStatusCode.OK });
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ Assert.ok(item.data, "Data should exist");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.equal(item.data!.baseType, "RemoteDependencyData", "BaseType should be RemoteDependencyData");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.ok(item.data!.baseData, "BaseData should exist");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.equal(item.data!.baseData.name, "GET /data", "Name should match span name");
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.equal(item.data!.baseData.success, true, "Success should be true for OK status");
+ }
+ });
+
+ this.testCase({
+ name: "AnalyticsPlugin: PRODUCER span creates RemoteDependencyData with message type",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("send-message", {
+ kind: eOTelSpanKind.PRODUCER,
+ attributes: {
+ "messaging.system": "rabbitmq",
+ "messaging.destination": "orders-queue",
+ "messaging.operation": "send"
+ }
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ Assert.ok(item.data, "Data should exist");
+ if (!item.data) {
+ return;
+ }
+ Assert.equal(item.data.baseType, "RemoteDependencyData", "BaseType should be RemoteDependencyData");
+ Assert.ok(item.data.baseData, "BaseData should exist");
+ if (!item.data.baseData) {
+ return;
+ }
+ Assert.ok(item.data.baseData.type, "Type should be set for message dependency");
+ }
+ });
+
+ this.testCase({
+ name: "AnalyticsPlugin: custom properties merged into baseData",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("operation-with-props", {
+ kind: eOTelSpanKind.INTERNAL,
+ attributes: {
+ "custom.string": "value",
+ "custom.number": 42,
+ "custom.boolean": true
+ }
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ span.setAttribute("runtime.prop", "added-after-start");
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ if (!item.data) {
+ return;
+ }
+ Assert.ok(item.data.baseData, "BaseData should exist");
+ if (!item.data.baseData) {
+ return;
+ }
+
+ // Custom properties should be in properties object
+ if (item.data.baseData.properties) {
+ Assert.equal(item.data.baseData.properties["custom.string"], "value", "String property should be preserved");
+ Assert.equal(item.data.baseData.properties["custom.number"], 42, "Number property should be preserved");
+ Assert.equal(item.data.baseData.properties["custom.boolean"], "true", "Boolean property should be preserved");
+ Assert.equal(item.data.baseData.properties["runtime.prop"], "added-after-start",
+ "Runtime-added property should be present");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "AnalyticsPlugin: span duration calculated correctly",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("timed-operation", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ // Simulate some time passing
+ this.clock.tick(250);
+
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ if (!item.data) {
+ return;
+ }
+ Assert.ok(item.data.baseData, "BaseData should exist");
+ if (!item.data.baseData) {
+ return;
+ }
+ Assert.equal(item.data.baseData.name, "timed-operation", "Name should match span name");
+ Assert.ok(item.data.baseData.duration, "Duration should exist");
+
+ // Duration should be approximately 250ms (formatted as time span string)
+ const durationMs = this.parseDurationToMs(item.data.baseData.duration);
+ Assert.ok(durationMs >= 240 && durationMs <= 260,
+ "Duration should be ~250ms, got " + durationMs + "ms - " + JSON.stringify(item));
+ }
+ });
+
+ this.testCase({
+ name: "AnalyticsPlugin: failed span sets success=false",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("failing-operation", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ span.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Operation failed"
+ });
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ if (!item.data) {
+ return;
+ }
+ Assert.ok(item.data.baseData, "BaseData should exist");
+ if (!item.data.baseData) {
+ return;
+ }
+ Assert.equal(item.data.baseData.success, false, "Success should be false for ERROR status");
+ }
+ });
+ }
+
+ private addTelemetryInitializerTests(): void {
+ this.testCase({
+ name: "TelemetryInitializer: can modify span telemetry",
+ useFakeTimers: true,
+ test: () => {
+ let initializerCalled = false;
+
+ this._ai.addTelemetryInitializer((item: ITelemetryItem) => {
+ initializerCalled = true;
+
+ if (item.baseType === "RemoteDependencyData") {
+ // Add custom property via initializer
+ item.baseData = item.baseData || {};
+ item.baseData.properties = item.baseData.properties || {};
+ item.baseData.properties["initializer.added"] = "custom-value";
+ item.baseData.properties["initializer.timestamp"] = new Date().toISOString();
+ }
+
+ return true;
+ });
+
+ const span = this._ai.startSpan("initialized-span", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ span.end();
+ this.clock.tick(500);
+
+ Assert.ok(initializerCalled, "Telemetry initializer should be called");
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Telemetry should be sent");
+
+ const item = sentItems[0] as ITelemetryItem;
+ if (!item.data) {
+ return;
+ }
+ if (!item.data.baseData) {
+ return;
+ }
+ Assert.ok(item.data.baseData.properties, "Properties should exist");
+ Assert.equal(item.data.baseData.properties["initializer.added"], "custom-value",
+ "Initializer-added property should be present");
+ Assert.ok(item.data.baseData.properties["initializer.timestamp"],
+ "Timestamp should be added by initializer");
+ }
+ });
+
+ this.testCase({
+ name: "TelemetryInitializer: can filter span telemetry",
+ useFakeTimers: true,
+ test: () => {
+ this._ai.addTelemetryInitializer((item: ITelemetryItem) => {
+ // Filter out spans with specific attribute
+ if (item.baseType === "RemoteDependencyData" &&
+ item.baseData &&
+ item.baseData.properties &&
+ item.baseData.properties["filter.me"] === "true") {
+ return false; // Reject this telemetry
+ }
+ return true;
+ });
+
+ // This span should be filtered out
+ const filteredSpan = this._ai.startSpan("filtered-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "filter.me": "true"
+ }
+ });
+ Assert.ok(filteredSpan, "Filtered span should be created");
+ if (filteredSpan) {
+ filteredSpan.end();
+ }
+
+ // This span should go through
+ const normalSpan = this._ai.startSpan("normal-span", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(normalSpan, "Normal span should be created");
+ if (normalSpan) {
+ normalSpan.end();
+ }
+
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(sentItems.length, 1, "Only one span should be sent (filtered one rejected)");
+
+ const item = sentItems[0] as ITelemetryItem;
+ if (item.data && item.data.baseData) {
+ Assert.equal(item.data.baseData.name, "normal-span", "Only normal span should be sent");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "TelemetryInitializer: can enrich with context data",
+ useFakeTimers: true,
+ test: () => {
+ this._ai.addTelemetryInitializer((item: ITelemetryItem) => {
+ // Add environment and build info to all span telemetry
+ if (item.baseType === "RemoteDependencyData") {
+ item.baseData = item.baseData || {};
+ item.baseData.properties = item.baseData.properties || {};
+ item.baseData.properties["environment"] = "test";
+ item.baseData.properties["build.version"] = "1.2.3";
+ item.baseData.properties["region"] = "us-west";
+ }
+ return true;
+ });
+
+ const span = this._ai.startSpan("enriched-span", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const item = sentItems[0] as ITelemetryItem;
+ if (!item.data) {
+ return;
+ }
+ if (!item.data.baseData) {
+ return;
+ }
+
+ Assert.equal(item.data.baseData.properties["environment"], "test", "Environment should be added");
+ Assert.equal(item.data.baseData.properties["build.version"], "1.2.3", "Build version should be added");
+ Assert.equal(item.data.baseData.properties["region"], "us-west", "Region should be added");
+ }
+ });
+ }
+
+ private addSessionContextTests(): void {
+ this.testCase({
+ name: "SessionContext: consistent session ID across multiple spans",
+ useFakeTimers: true,
+ test: () => {
+ const span1 = this._ai.startSpan("operation-1", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span1, "First span should be created");
+ if (span1) {
+ span1.end();
+ }
+
+ const span2 = this._ai.startSpan("operation-2", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span2, "Second span should be created");
+ if (span2) {
+ span2.end();
+ }
+
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(sentItems.length, 2, "Two telemetry items should be sent");
+
+ const payload1 = sentItems[0];
+ const payload2 = sentItems[1];
+
+ const sessionId1 = payload1.tags ? payload1.tags["ai.session.id"] : undefined;
+ const sessionId2 = payload2.tags ? payload2.tags["ai.session.id"] : undefined;
+
+ Assert.ok(sessionId1, "First item should have session ID");
+ Assert.ok(sessionId2, "Second item should have session ID");
+ Assert.equal(sessionId1, sessionId2, "Session IDs should be consistent");
+ }
+ });
+
+ this.testCase({
+ name: "SessionContext: session renewal doesn't affect active spans",
+ useFakeTimers: true,
+ test: () => {
+ const span1 = this._ai.startSpan("before-renewal", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span1, "Span before renewal should be created");
+ if (span1) {
+ span1.end();
+ }
+ this.clock.tick(500);
+
+ // Simulate session renewal time passing (30+ minutes)
+ this.clock.tick(31 * 60 * 1000);
+
+ const span2 = this._ai.startSpan("after-renewal", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span2, "Span after renewal should be created");
+ if (span2) {
+ span2.end();
+ }
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(sentItems.length, 2, "Both spans should be sent");
+
+ // Session might have renewed, but both spans should have valid session IDs
+ const payload1 = sentItems[0];
+ const payload2 = sentItems[1];
+
+ const sessionId1 = payload1.tags ? payload1.tags["ai.session.id"] : undefined;
+ const sessionId2 = payload2.tags ? payload2.tags["ai.session.id"] : undefined;
+
+ Assert.ok(sessionId1, "First span should have session ID");
+ Assert.ok(sessionId2, "Second span should have session ID");
+ }
+ });
+ }
+
+ private addUserContextTests(): void {
+ this.testCase({
+ name: "UserContext: setting user ID applies to subsequent spans",
+ useFakeTimers: true,
+ test: () => {
+ // Set user context
+ this._ai.context.user.id = "user-12345";
+ this._ai.context.user.authenticatedId = "auth-user-67890";
+
+ const span = this._ai.startSpan("user-operation", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const payload = sentItems[0];
+
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // User ID is sent in tags with key "ai.user.id"
+ const userId = payload.tags["ai.user.id"];
+ Assert.equal(userId, "user-12345", "User ID should match");
+
+ // Auth user ID is sent in tags with key "ai.user.authUserId"
+ const authUserId = payload.tags["ai.user.authUserId"];
+ Assert.equal(authUserId, "auth-user-67890", "Authenticated ID should match");
+ }
+ });
+
+ this.testCase({
+ name: "UserContext: clearing user context removes from spans",
+ useFakeTimers: true,
+ test: () => {
+ // Set then clear
+ this._ai.context.user.authenticatedId = "temp-user";
+ this._ai.context.user.clearAuthenticatedUserContext();
+
+ const span = this._ai.startSpan("after-clear", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const payload = sentItems[0];
+
+ // User context should still exist but authenticated ID should be undefined/missing
+ if (payload.tags) {
+ const authUserId = payload.tags["ai.user.authUserId"];
+ Assert.ok(!authUserId || authUserId === undefined,
+ "Authenticated ID should be cleared");
+ }
+ }
+ });
+ }
+
+ private addDeviceContextTests(): void {
+ this.testCase({
+ name: "DeviceContext: device information included in all spans",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("device-check", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const payload = sentItems[0];
+
+ Assert.ok(payload.tags, "Payload should have tags");
+
+ // Device info is sent in tags
+ const deviceType = payload.tags["ai.device.type"];
+ const deviceId = payload.tags["ai.device.id"];
+
+ Assert.ok(deviceType, "Device type should be set");
+ Assert.ok(deviceId, "Device ID should be set");
+ }
+ });
+ }
+
+ private addDistributedTraceContextTests(): void {
+ this.testCase({
+ name: "DistributedTrace: parent-child spans share trace ID",
+ useFakeTimers: true,
+ test: () => {
+ const parentSpan = this._ai.startSpan("parent-op", {
+ kind: eOTelSpanKind.SERVER
+ });
+ Assert.ok(parentSpan, "Parent span should be created");
+ if (!parentSpan) {
+ return;
+ }
+
+ const childSpan = this._ai.startSpan("child-op", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(childSpan, "Child span should be created");
+
+ parentSpan.end();
+ if (childSpan) {
+ childSpan.end();
+ }
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(sentItems.length, 2, "Both spans should be sent");
+
+ const parentPayload = sentItems[0];
+ const childPayload = sentItems[1];
+
+ // Both should have same operation ID (trace ID) in tags
+ const parentOpId = parentPayload.tags ? parentPayload.tags["ai.operation.id"] : undefined;
+ const childOpId = childPayload.tags ? childPayload.tags["ai.operation.id"] : undefined;
+
+ Assert.ok(parentOpId, "Parent should have operation ID");
+ Assert.ok(childOpId, "Child should have operation ID");
+ Assert.equal(parentOpId, childOpId, "Trace IDs should match for parent and child");
+ }
+ });
+
+ this.testCase({
+ name: "DistributedTrace: span context propagates through telemetry",
+ useFakeTimers: true,
+ test: () => {
+ const span = this._ai.startSpan("traced-operation", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+
+ const spanContext = span.spanContext();
+ Assert.ok(spanContext.traceId, "Span should have trace ID");
+ Assert.ok(spanContext.spanId, "Span should have span ID");
+
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const payload = sentItems[0];
+
+ // Trace context should be in tags
+ if (payload.tags) {
+ const operationId = payload.tags["ai.operation.id"];
+ const operationParentId = payload.tags["ai.operation.parentId"];
+
+ Assert.equal(operationId, spanContext.traceId, "Operation ID should match trace ID");
+ Assert.ok(operationParentId, "Operation parent ID should be set");
+ }
+ }
+ });
+ }
+
+ private addSamplingIntegrationTests(): void {
+ this.testCase({
+ name: "Sampling: 1% sampling allows minimal span telemetry",
+ useFakeTimers: true,
+ test: () => {
+ // Recreate AI with 1% sampling (minimum valid value)
+ this._ai.unload(false);
+
+ this._ai = new ApplicationInsights({
+ config: {
+ instrumentationKey: "test-ikey-123",
+ samplingPercentage: 1
+ }
+ });
+ this._ai.loadAppInsights();
+
+ const span = this._ai.startSpan("low-sampled", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should still be created");
+ if (span) {
+ span.end();
+ }
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ // With 1% sampling, telemetry may or may not be sent (depends on sample hash)
+ Assert.ok(sentItems.length >= 0, "Telemetry should respect 1% sampling rate");
+ }
+ });
+
+ this.testCase({
+ name: "Sampling: 100% sampling sends all span telemetry",
+ useFakeTimers: true,
+ test: () => {
+ // Default config has 100% sampling
+ const spans = [];
+ for (let i = 0; i < 10; i++) {
+ const span = this._ai.startSpan("operation-" + i, {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span " + i + " should be created");
+ if (span) {
+ span.end();
+ spans.push(span);
+ }
+ }
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.equal(sentItems.length, 10, "All 10 spans should be sent with 100% sampling");
+ }
+ });
+ }
+
+ private addConfigurationIntegrationTests(): void {
+ this.testCase({
+ name: "Config: disableAjaxTracking doesn't affect manual spans",
+ useFakeTimers: true,
+ test: () => {
+ // Config already has disableAjaxTracking: false, but manual spans should work regardless
+ const span = this._ai.startSpan("manual-span", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ Assert.ok(span, "Manual span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Manual span should be sent regardless of ajax tracking config");
+ }
+ });
+
+ this.testCase({
+ name: "Config: maxBatchInterval affects when span telemetry is sent",
+ useFakeTimers: true,
+ test: () => {
+ // Current config has maxBatchInterval: 0 (send immediately)
+ const span = this._ai.startSpan("immediate-send", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+
+ // No tick needed with maxBatchInterval: 0
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ Assert.ok(sentItems.length > 0, "Span should be sent immediately");
+ }
+ });
+
+ this.testCase({
+ name: "Config: extensionConfig reaches PropertiesPlugin",
+ useFakeTimers: true,
+ test: () => {
+ // We set accountId in extensionConfig during init
+ const span = this._ai.startSpan("config-test", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span, "Span should be created");
+ if (!span) {
+ return;
+ }
+ span.end();
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+ const payload = sentItems[0];
+
+ // Check if account ID from config made it through tags
+ if (payload.tags) {
+ const accountId = payload.tags["ai.user.accountId"];
+ if (accountId) {
+ Assert.equal(accountId, "test-account-id",
+ "Account ID from config should be present in tags");
+ }
+ }
+ }
+ });
+
+ this.testCase({
+ name: "Config: dynamic configuration changes affect new spans",
+ useFakeTimers: true,
+ test: () => {
+ // Create span with initial config
+ const span1 = this._ai.startSpan("before-config-change", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span1, "First span should be created");
+ if (span1) {
+ span1.end();
+ }
+ this.clock.tick(500);
+
+ // Change configuration dynamically
+ this._ai.config.extensionConfig = this._ai.config.extensionConfig || {};
+ this._ai.config.extensionConfig["AppInsightsChannelPlugin"] =
+ this._ai.config.extensionConfig["AppInsightsChannelPlugin"] || {};
+ this._ai.config.extensionConfig["AppInsightsChannelPlugin"].samplingPercentage = 1;
+ this.clock.tick(500); // Allow config change to propagate
+
+ // Create span after config change
+ const span2 = this._ai.startSpan("after-config-change", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(span2, "Second span should be created");
+ if (span2) {
+ span2.end();
+ }
+ this.clock.tick(500);
+
+ const sentItems = this.getSentTelemetry();
+
+ // First span should be sent (100% default), second may be sampled (1%)
+ Assert.ok(sentItems.length >= 1, "At least first span should be sent");
+
+ const firstItem = sentItems[0] as ITelemetryItem;
+ if (firstItem.data && firstItem.data.baseData) {
+ Assert.equal(firstItem.data.baseData.name, "before-config-change",
+ "First span should be sent before config change");
+ }
+ }
+ });
+ }
+
+ // Helper methods
+ private getSentTelemetry(): any[] {
+ const items: any[] = [];
+ const requests = this.activeXhrRequests;
+ if (requests) {
+ requests.forEach((request: any) => {
+ if (request.requestBody) {
+ try {
+ const payload = JSON.parse(request.requestBody);
+ if (payload && Array.isArray(payload)) {
+ items.push(...payload);
+ } else if (payload) {
+ items.push(payload);
+ }
+ } catch (e) {
+ // Ignore parse errors
+ }
+ }
+ });
+ }
+ return items;
+ }
+
+ private parseDurationToMs(duration: string): number {
+ // Duration format: "00:00:00.250" or similar
+ if (!duration) {
+ return 0;
+ }
+
+ const parts = duration.split(":");
+ if (parts.length !== 3) {
+ return 0;
+ }
+
+ const hours = parseInt(parts[0], 10);
+ const minutes = parseInt(parts[1], 10);
+ const secondsParts = parts[2].split(".");
+ const seconds = parseInt(secondsParts[0], 10);
+ const milliseconds = secondsParts[1] ? parseInt(secondsParts[1].padEnd(3, "0").substring(0, 3), 10) : 0;
+
+ return (hours * 3600000) + (minutes * 60000) + (seconds * 1000) + milliseconds;
+ }
+}
diff --git a/AISKU/Tests/Unit/src/SpanUtils.Tests.ts b/AISKU/Tests/Unit/src/SpanUtils.Tests.ts
new file mode 100644
index 000000000..22ee416d7
--- /dev/null
+++ b/AISKU/Tests/Unit/src/SpanUtils.Tests.ts
@@ -0,0 +1,1971 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights, IDependencyTelemetry } from "../../../src/applicationinsights-web";
+import {
+ eOTelSpanKind,
+ eOTelSpanStatusCode,
+ ITelemetryItem,
+ SEMATTRS_HTTP_METHOD,
+ SEMATTRS_HTTP_URL,
+ SEMATTRS_HTTP_STATUS_CODE,
+ SEMATTRS_DB_SYSTEM,
+ SEMATTRS_DB_STATEMENT,
+ SEMATTRS_DB_NAME,
+ SEMATTRS_RPC_SYSTEM,
+ SEMATTRS_RPC_GRPC_STATUS_CODE,
+ ATTR_HTTP_REQUEST_METHOD,
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
+ ATTR_URL_FULL,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+ ATTR_ENDUSER_ID,
+ ATTR_ENDUSER_PSEUDO_ID,
+ ATTR_HTTP_ROUTE,
+ MicrosoftClientIp
+} from "@microsoft/applicationinsights-core-js";
+import { IRequestTelemetry } from "@microsoft/applicationinsights-core-js";
+
+export class SpanUtilsTests extends AITestClass {
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${SpanUtilsTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "SpanUtilsTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: SpanUtilsTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+ } catch (e) {
+ console.error("Failed to initialize tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addDependencyTelemetryTests();
+ this.addRequestTelemetryTests();
+ this.addHttpDependencyTests();
+ this.addDbDependencyTests();
+ this.addRpcDependencyTests();
+ this.addAttributeMappingTests();
+ this.addTagsCreationTests();
+ this.addAzureSDKTests();
+ this.addSemanticAttributeExclusionTests();
+ this.addEdgeCaseTests();
+ this.addCrossBrowserCompatibilityTests();
+ }
+
+ private addDependencyTelemetryTests(): void {
+ this.testCase({
+ name: "createDependencyTelemetry: CLIENT span generates RemoteDependency telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("client-operation", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "custom.attr": "value"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.equal(item.name, "Microsoft.ApplicationInsights.RemoteDependency", "Should be RemoteDependency");
+ Assert.equal(item.baseType, "RemoteDependencyData", "Should have correct baseType");
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.equal((item.baseData as any).name, "client-operation", "Should have span name");
+ Assert.equal((item.baseData as any).type, "Dependency", "Should have default dependency type");
+ }
+ });
+
+ this.testCase({
+ name: "createDependencyTelemetry: PRODUCER span generates QueueMessage dependency",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("queue-producer", {
+ kind: eOTelSpanKind.PRODUCER,
+ attributes: {
+ "messaging.system": "kafka",
+ "messaging.destination": "orders"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "Queue Message", "Should be QueueMessage type");
+ }
+ });
+
+ this.testCase({
+ name: "createDependencyTelemetry: INTERNAL span with parent generates InProc dependency",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const parentSpan = this._ai.startSpan("parent-operation");
+ const parentContext = parentSpan?.spanContext();
+
+ const childSpan = this._ai.startSpan("internal-operation", {
+ kind: eOTelSpanKind.INTERNAL
+ }, parentContext);
+ childSpan?.end();
+ parentSpan?.end();
+
+ // Assert
+ const childItem = this._trackCalls.find(t => t.baseData?.name === "internal-operation");
+ Assert.ok(childItem, "Should have child telemetry");
+ Assert.equal((childItem?.baseData as any).type, "InProc", "Should be InProc type");
+ }
+ });
+
+ this.testCase({
+ name: "createDependencyTelemetry: SUCCESS status based on span status code",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - span with OK status
+ const okSpan = this._ai.startSpan("ok-span", { kind: eOTelSpanKind.CLIENT });
+ okSpan?.setStatus({ code: eOTelSpanStatusCode.OK });
+ okSpan?.end();
+
+ // Act - span with ERROR status
+ const errorSpan = this._ai.startSpan("error-span", { kind: eOTelSpanKind.CLIENT });
+ errorSpan?.setStatus({ code: eOTelSpanStatusCode.ERROR, message: "Failed" });
+ errorSpan?.end();
+
+ // Assert
+ const okItem = this._trackCalls.find(t => t.baseData?.name === "ok-span");
+ const errorItem = this._trackCalls.find(t => t.baseData?.name === "error-span");
+
+ Assert.equal((okItem?.baseData as any).success, true, "OK span should have success=true");
+ Assert.equal((errorItem?.baseData as any).success, false, "ERROR span should have success=false");
+ }
+ });
+
+ this.testCase({
+ name: "createDependencyTelemetry: includes span context IDs",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("test-span", { kind: eOTelSpanKind.CLIENT });
+ const spanContext = span?.spanContext();
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).id, spanContext?.spanId, "Should have spanId");
+ Assert.ok(item.tags, "Should have tags");
+ Assert.equal((item.tags as any)["ai.operation.id"], spanContext?.traceId, "Should have traceId in tags");
+ }
+ });
+ }
+
+ private addRequestTelemetryTests(): void {
+ this.testCase({
+ name: "createRequestTelemetry: SERVER span generates Request telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("server-operation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://example.com/api/users"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.equal(item.name, "Microsoft.ApplicationInsights.Request", "Should be Request");
+ Assert.equal(item.baseType, "RequestData", "Should have correct baseType");
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "createRequestTelemetry: CONSUMER span generates Request telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("queue-consumer", {
+ kind: eOTelSpanKind.CONSUMER,
+ attributes: {
+ "messaging.system": "rabbitmq"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal(item.name, "Microsoft.ApplicationInsights.Request", "Should be Request");
+ }
+ });
+
+ this.testCase({
+ name: "createRequestTelemetry: SUCCESS derived from status code",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - UNSET status with 2xx HTTP code
+ const successSpan = this._ai.startSpan("success-request", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "POST",
+ "http.status_code": 201
+ }
+ });
+ successSpan?.end();
+
+ // Act - UNSET status with 5xx HTTP code
+ const failSpan = this._ai.startSpan("fail-request", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "GET",
+ "http.status_code": 500
+ }
+ });
+ failSpan?.end();
+
+ // Assert
+ const successItem = this._trackCalls.find(t => t.baseData?.name === "success-request");
+ const failItem = this._trackCalls.find(t => t.baseData?.name === "fail-request");
+
+ Assert.equal((successItem?.baseData as any).success, true, "2xx should be success");
+ Assert.equal((failItem?.baseData as any).success, false, "5xx should be failure");
+ }
+ });
+
+ this.testCase({
+ name: "createRequestTelemetry: OK status overrides HTTP status code",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - OK status with 5xx code (shouldn't happen but testing precedence)
+ const span = this._ai.startSpan("explicit-ok", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "GET",
+ "http.status_code": 500
+ }
+ });
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).success, true, "OK status should take precedence");
+ }
+ });
+
+ this.testCase({
+ name: "createRequestTelemetry: includes URL for HTTP requests",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const testUrl = "https://api.example.com/v1/users?id=123";
+
+ // Act
+ const span = this._ai.startSpan("http-request", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "GET",
+ "http.url": testUrl,
+ "http.status_code": 200
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const baseData = item.baseData as IRequestTelemetry;
+ Assert.equal(baseData.url, testUrl, "Should include URL");
+ Assert.equal(baseData.responseCode, 200, "Should include status code");
+ }
+ });
+
+ this.testCase({
+ name: "createRequestTelemetry: gRPC status code mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("grpc-request", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "rpc.system": "grpc",
+ "rpc.grpc.status_code": 0 // OK
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const baseData = item.baseData as IRequestTelemetry;
+ Assert.equal(baseData.responseCode, 0, "Should map gRPC status code");
+ }
+ });
+ }
+
+ private addHttpDependencyTests(): void {
+ this.testCase({
+ name: "HTTP Dependency: legacy semantic conventions mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_HTTP_METHOD]: "POST",
+ [SEMATTRS_HTTP_URL]: "https://api.example.com/v1/users",
+ [SEMATTRS_HTTP_STATUS_CODE]: 201
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const baseData = item.baseData as IDependencyTelemetry;
+ Assert.equal(baseData.type, "Http", "Should be HTTP type");
+ Assert.ok(baseData.name?.startsWith("POST"), "Name should include method");
+ Assert.equal(baseData.data, "https://api.example.com/v1/users", "Should include URL");
+ Assert.equal(baseData.responseCode, 201, "Should include status code");
+ }
+ });
+
+ this.testCase({
+ name: "HTTP Dependency: new semantic conventions mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-call-new", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [ATTR_HTTP_REQUEST_METHOD]: "GET",
+ [ATTR_URL_FULL]: "https://api.example.com/v2/products",
+ [ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
+ [ATTR_SERVER_ADDRESS]: "api.example.com",
+ [ATTR_SERVER_PORT]: 443
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const baseData = item.baseData as IDependencyTelemetry;
+ Assert.equal(baseData.type, "Http", "Should be HTTP type");
+ Assert.ok(baseData.data, "Should have data field");
+ }
+ });
+
+ this.testCase({
+ name: "HTTP Dependency: target with default port removal",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - HTTPS with default port 443
+ const httpsSpan = this._ai.startSpan("https-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://example.com:443/api",
+ "net.peer.name": "example.com",
+ "net.peer.port": 443
+ }
+ });
+ httpsSpan?.end();
+
+ // Act - HTTP with default port 80
+ const httpSpan = this._ai.startSpan("http-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "http://example.com:80/api",
+ "net.peer.name": "example.com",
+ "net.peer.port": 80
+ }
+ });
+ httpSpan?.end();
+
+ // Assert
+ const httpsItem = this._trackCalls.find(t => t.baseData?.name?.includes("https-call") || t.baseData?.data?.includes("https://example.com:443"));
+ const httpItem = this._trackCalls.find(t => t.baseData?.name?.includes("http-call") || t.baseData?.data?.includes("http://example.com:80"));
+
+ // Default ports should be stripped from target
+ Assert.ok(httpsItem, "Should have HTTPS telemetry");
+ Assert.ok(httpItem, "Should have HTTP telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "HTTP Dependency: target with non-default port preserved",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("custom-port-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://example.com:8443/api",
+ "net.peer.name": "example.com",
+ "net.peer.port": 8443
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).target, "Should have target");
+ // Non-default port should be preserved in target
+ }
+ });
+
+ this.testCase({
+ name: "HTTP Dependency: name generated from URL pathname",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("generic-name", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "DELETE",
+ "http.url": "https://api.example.com/v1/users/123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).name.includes("DELETE"), "Name should include HTTP method");
+ Assert.ok((item.baseData as any).name.includes("/v1/users/123"), "Name should include path");
+ }
+ });
+ }
+
+ private addDbDependencyTests(): void {
+ this.testCase({
+ name: "DB Dependency: MySQL mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("db-query", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "mysql",
+ [SEMATTRS_DB_STATEMENT]: "SELECT * FROM users WHERE id = ?",
+ [SEMATTRS_DB_NAME]: "myapp_db",
+ "net.peer.name": "db.example.com",
+ "net.peer.port": 3306
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "mysql", "Should be mysql type");
+ Assert.equal((item.baseData as any).data, "SELECT * FROM users WHERE id = ?", "Should include statement");
+ Assert.ok((item.baseData as any).target?.includes("myapp_db"), "Target should include DB name");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: PostgreSQL mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("postgres-query", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "postgresql",
+ [SEMATTRS_DB_STATEMENT]: "INSERT INTO logs (message) VALUES ($1)"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "postgresql", "Should be postgresql type");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: MongoDB mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("mongo-query", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "mongodb",
+ [SEMATTRS_DB_STATEMENT]: "db.users.find({age: {$gt: 25}})"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "mongodb", "Should be mongodb type");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: Redis mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("redis-cmd", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "redis",
+ [SEMATTRS_DB_STATEMENT]: "GET user:123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "redis", "Should be redis type");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: SQL Server mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("mssql-query", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "mssql",
+ [SEMATTRS_DB_STATEMENT]: "SELECT TOP 10 * FROM Orders"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "SQL", "Should be SQL type for SQL Server");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: operation used when no statement",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("db-op", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "postgresql",
+ "db.operation": "SELECT",
+ [SEMATTRS_DB_NAME]: "products_db"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).data, "SELECT", "Should use operation when no statement");
+ }
+ });
+
+ this.testCase({
+ name: "DB Dependency: target formatting with host and dbname",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("db-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "mysql",
+ [SEMATTRS_DB_NAME]: "production_db",
+ "net.peer.name": "mysql-prod.example.com",
+ "net.peer.port": 3306
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).target?.includes("mysql-prod.example.com"), "Target should include host");
+ Assert.ok((item.baseData as any).target?.includes("production_db"), "Target should include DB name");
+ }
+ });
+ }
+
+ private addRpcDependencyTests(): void {
+ this.testCase({
+ name: "RPC Dependency: gRPC mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("grpc-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_RPC_SYSTEM]: "grpc",
+ [SEMATTRS_RPC_GRPC_STATUS_CODE]: 0,
+ "rpc.service": "UserService",
+ "rpc.method": "GetUser"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ let baseData = item.baseData as IDependencyTelemetry;
+ Assert.equal(baseData.type, "GRPC", "Should be Dependency type");
+ Assert.equal(baseData.responseCode, 0, "Should include gRPC status code");
+ }
+ });
+
+ this.testCase({
+ name: "RPC Dependency: WCF mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("wcf-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_RPC_SYSTEM]: "wcf",
+ "rpc.service": "CalculatorService"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "WCF Service", "Should be Dependency type");
+ }
+ });
+
+ this.testCase({
+ name: "RPC Dependency: target from peer service",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("rpc-call", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_RPC_SYSTEM]: "grpc",
+ "net.peer.name": "grpc.example.com",
+ "net.peer.port": 50051
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ let baseData = item.baseData as IDependencyTelemetry;
+ Assert.ok(baseData.target, "Should have target");
+ }
+ });
+ }
+
+ private addAttributeMappingTests(): void {
+ this.testCase({
+ name: "Attribute Mapping: custom attributes preserved in properties",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("custom-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "app.version": "1.2.3",
+ "user.tier": "premium",
+ "request.priority": 5,
+ "feature.enabled": true
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).properties, "Should have properties");
+ Assert.equal((item.baseData as any).properties["app.version"], "1.2.3", "String attribute preserved");
+ Assert.equal((item.baseData as any).properties["user.tier"], "premium", "String attribute preserved");
+ Assert.equal((item.baseData as any).properties["request.priority"], 5, "Number attribute preserved");
+ Assert.equal((item.baseData as any).properties["feature.enabled"], true, "Boolean attribute preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Attribute Mapping: dt.spanId and dt.traceId always added",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("test-span", { kind: eOTelSpanKind.CLIENT });
+ const context = span?.spanContext();
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal(item.ext?.dt.spanId, context?.spanId, "Should have dt.spanId");
+ Assert.equal(item.ext?.dt.traceId, context?.traceId, "Should have dt.traceId");
+ }
+ });
+
+ this.testCase({
+ name: "Attribute Mapping: sampling.probability mapped to sampleRate",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("sampled-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "microsoft.sample_rate": 25
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0] as any;
+ Assert.equal(item.sampleRate, 25, "Should map sampling.probability to sampleRate");
+ }
+ });
+ }
+
+ private addTagsCreationTests(): void {
+ this.testCase({
+ name: "Tags: operation ID from trace ID",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("test-span", { kind: eOTelSpanKind.SERVER });
+ const context = span?.spanContext();
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.tags as any)["ai.operation.id"], context?.traceId, "Should map traceId to operation.id");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: operation parent ID from parent span",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const parentSpan = this._ai.startSpan("parent", { kind: eOTelSpanKind.SERVER });
+ const parentContext = parentSpan?.spanContext();
+
+ const childSpan = this._ai.startSpan("child", { kind: eOTelSpanKind.INTERNAL }, parentContext);
+ childSpan?.end();
+ parentSpan?.end();
+
+ // Assert
+ const childItem = this._trackCalls.find(t => t.baseData?.name === "child");
+ Assert.equal((childItem?.tags as any)?.["ai.operation.parentId"], parentContext?.spanId,
+ "Should map parent spanId to operation.parentId");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: enduser.id mapped to user auth ID",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("user-span", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ [ATTR_ENDUSER_ID]: "user@example.com"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.tags as any)["ai.user.authUserId"], "user@example.com",
+ "Should map enduser.id to user.authUserId");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: enduser.pseudo.id mapped to user ID",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("pseudo-user-span", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ [ATTR_ENDUSER_PSEUDO_ID]: "anon-12345"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.tags as any)["ai.user.id"], "anon-12345",
+ "Should map enduser.pseudo.id to user.id");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: microsoft.client.ip takes precedence",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const clientIp = "203.0.113.42";
+
+ // Act
+ const span = this._ai.startSpan("ip-span", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ [MicrosoftClientIp]: clientIp,
+ "client.address": "192.168.1.1", // Should be ignored
+ "http.client_ip": "10.0.0.1" // Should be ignored
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.tags as any)["ai.location.ip"], clientIp,
+ "microsoft.client.ip should take precedence");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: operation name from http.route for SERVER spans",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-request", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "POST",
+ [ATTR_HTTP_ROUTE]: "/api/v1/users/:id",
+ "http.url": "https://example.com/api/v1/users/123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.tags as any)["ai.operation.name"]?.includes("POST"), "Should include method");
+ Assert.ok((item.tags as any)["ai.operation.name"]?.includes("/api/v1/users/:id"), "Should include route");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: operation name falls back to URL path when no route",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-request-no-route", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://example.com/products/search?q=laptop"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.tags as any)["ai.operation.name"]?.includes("GET"), "Should include method");
+ Assert.ok((item.tags as any)["ai.operation.name"]?.includes("/products/search"), "Should include path");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: user agent mapped correctly",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0";
+
+ // Act
+ const span = this._ai.startSpan("ua-span", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.user_agent": userAgent
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.tags as any)["ai.user.userAgent"], userAgent, "Should map user agent");
+ }
+ });
+
+ this.testCase({
+ name: "Tags: synthetic source detection",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("bot-span", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.user_agent": "Googlebot/2.1 (+http://www.google.com/bot.html)"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ // Synthetic source should be detected for bot user agents
+ if ((item.tags as any)["ai.operation.syntheticSource"]) {
+ Assert.equal((item.tags as any)["ai.operation.syntheticSource"], "True",
+ "Should detect synthetic source for bots");
+ }
+ }
+ });
+ }
+
+ private addAzureSDKTests(): void {
+ this.testCase({
+ name: "Azure SDK: EventHub PRODUCER span mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("EventHubs.send", {
+ kind: eOTelSpanKind.PRODUCER,
+ attributes: {
+ "az.namespace": "Microsoft.EventHub",
+ "message_bus.destination": "telemetry-events",
+ "net.peer.name": "eventhub.servicebus.windows.net"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).type?.includes("Queue Message"), "Should be Queue Message type");
+ Assert.ok((item.baseData as any).type?.includes("Microsoft.EventHub"), "Should include namespace");
+ Assert.ok((item.baseData as any).target, "Should have target");
+ }
+ });
+
+ this.testCase({
+ name: "Azure SDK: EventHub CONSUMER span mapping",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("EventHubs.process", {
+ kind: eOTelSpanKind.CONSUMER,
+ attributes: {
+ "az.namespace": "Microsoft.EventHub",
+ "message_bus.destination": "telemetry-events",
+ "net.peer.name": "eventhub.servicebus.windows.net",
+ "enqueuedTime": "1638360000000"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).source, "Consumer should have source");
+ Assert.ok((item.baseData as any).measurements, "Should have measurements");
+ Assert.ok("timeSinceEnqueued" in (item.baseData as any).measurements, "Should have timeSinceEnqueued measurement");
+ }
+ });
+
+ this.testCase({
+ name: "Azure SDK: INTERNAL span with Azure namespace",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("internal-azure-op", {
+ kind: eOTelSpanKind.INTERNAL,
+ attributes: {
+ "az.namespace": "Microsoft.Storage"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).type?.includes("InProc"), "Should include InProc");
+ Assert.ok((item.baseData as any).type?.includes("Microsoft.Storage"), "Should include namespace");
+ }
+ });
+ }
+
+ private addSemanticAttributeExclusionTests(): void {
+ this.testCase({
+ name: "Semantic Exclusion: HTTP attributes not in properties",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://example.com/api",
+ "http.status_code": 201,
+ "http.user_agent": "TestAgent/1.0",
+ "custom.attribute": "should-be-kept"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+
+ Assert.ok(!props["http.method"], "http.method should be excluded");
+ Assert.ok(!props["http.url"], "http.url should be excluded");
+ Assert.ok(!props["http.status_code"], "http.status_code should be excluded");
+ Assert.ok(!props["http.user_agent"], "http.user_agent should be excluded");
+ Assert.equal(props["custom.attribute"], "should-be-kept", "Custom attributes should be kept");
+ }
+ });
+
+ this.testCase({
+ name: "Semantic Exclusion: DB attributes not in properties",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("db-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "db.system": "postgresql",
+ "db.statement": "SELECT * FROM users",
+ "db.name": "mydb",
+ "db.operation": "SELECT",
+ "app.query.id": "query-123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+
+ Assert.ok(!props["db.system"], "db.system should be excluded");
+ Assert.ok(!props["db.statement"], "db.statement should be excluded");
+ Assert.ok(!props["db.name"], "db.name should be excluded");
+ Assert.ok(!props["db.operation"], "db.operation should be excluded");
+ Assert.equal(props["app.query.id"], "query-123", "Custom attributes should be kept");
+ }
+ });
+
+ this.testCase({
+ name: "Semantic Exclusion: microsoft.* attributes excluded",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("microsoft-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "microsoft.internal.flag": true,
+ "microsoft.client.ip": "192.168.1.1",
+ "microsoft.custom": "value",
+ "app.microsoft": "not-excluded"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+
+ Assert.ok(!props["microsoft.internal.flag"], "microsoft.* should be excluded");
+ Assert.ok(!props["microsoft.client.ip"], "microsoft.* should be excluded");
+ Assert.ok(!props["microsoft.custom"], "microsoft.* should be excluded");
+ Assert.equal(props["app.microsoft"], "not-excluded",
+ "Attributes containing 'microsoft' but not prefixed should be kept");
+ }
+ });
+
+ this.testCase({
+ name: "Semantic Exclusion: operation.name context tag excluded",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("op-name-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "ai.operation.name": "CustomOperation",
+ "custom.operation.name": "should-be-kept"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+
+ Assert.ok(!props["ai.operation.name"], "ai.operation.name should be excluded");
+ Assert.equal(props["custom.operation.name"], "should-be-kept",
+ "Similar named custom attributes should be kept");
+ }
+ });
+
+ this.testCase({
+ name: "Semantic Exclusion: new semantic conventions excluded",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("new-semconv", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [ATTR_HTTP_REQUEST_METHOD]: "GET",
+ [ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
+ [ATTR_URL_FULL]: "https://example.com",
+ [ATTR_SERVER_ADDRESS]: "example.com",
+ [ATTR_SERVER_PORT]: 443,
+ "app.request.id": "req-123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+
+ Assert.ok(!props["http.request.method"], "New http attributes should be excluded");
+ Assert.ok(!props["http.response.status_code"], "New http attributes should be excluded");
+ Assert.ok(!props["url.full"], "New url attributes should be excluded");
+ Assert.ok(!props["server.address"], "New server attributes should be excluded");
+ Assert.ok(!props["server.port"], "New server attributes should be excluded");
+ Assert.equal(props["app.request.id"], "req-123", "Custom attributes should be kept");
+ }
+ });
+ }
+
+ private addEdgeCaseTests(): void {
+ this.testCase({
+ name: "Edge Case: Empty span name",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry for empty name");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.equal((item.baseData as any).name, "", "Should preserve empty name");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with null/undefined attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("null-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "valid.attr": "value",
+ "null.attr": null as any,
+ "undefined.attr": undefined as any,
+ "zero.attr": 0,
+ "false.attr": false,
+ "empty.string": ""
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.equal(props["valid.attr"], "value", "Valid attributes should be preserved");
+ Assert.equal(props["zero.attr"], 0, "Zero values should be preserved");
+ Assert.equal(props["false.attr"], false, "False values should be preserved");
+ Assert.equal(props["empty.string"], "", "Empty strings should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with extremely long attribute values",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const veryLongValue = "a".repeat(20000);
+
+ // Act
+ const span = this._ai.startSpan("long-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "long.value": veryLongValue
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(props["long.value"], "Long value should be included");
+ Assert.equal(props["long.value"], veryLongValue, "Long value should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with special characters in name and attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("span-with-特殊字符-émojis-🎉", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "unicode.key": "value with 中文 and émojis 🚀",
+ "special.chars": "tab\there\nnewline\r\ncarriage",
+ "quotes": "\"double\" and 'single' quotes"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle special characters");
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).name.includes("特殊字符"), "Should preserve unicode in name");
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(props["unicode.key"], "Should preserve unicode attributes");
+ Assert.ok(props["special.chars"], "Should preserve special characters");
+ Assert.ok(props["quotes"], "Should preserve quotes");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span without explicit kind defaults appropriately",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - startSpan with no kind specified
+ const span = this._ai.startSpan("no-kind-span");
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item, "Should have telemetry item");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Multiple rapid span creations and endings",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spanCount = 50;
+
+ // Act - Create and end many spans rapidly
+ for (let i = 0; i < spanCount; i++) {
+ const span = this._ai.startSpan("rapid-span-" + i, {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "span.index": i
+ }
+ });
+ span?.end();
+ }
+
+ // Assert
+ Assert.equal(this._trackCalls.length, spanCount, "Should track all spans");
+ const firstItem = this._trackCalls[0];
+ const lastItem = this._trackCalls[spanCount - 1];
+ Assert.equal((firstItem.baseData as any).properties["span.index"], 0, "First span preserved");
+ Assert.equal((lastItem.baseData as any).properties["span.index"], spanCount - 1, "Last span preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with array attribute values",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("array-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "string.array": ["value1", "value2", "value3"],
+ "number.array": [1, 2, 3],
+ "mixed.array": ["string", 123, true] as any
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(props["string.array"], "Array attributes should be included");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with nested object attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("nested-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "nested.object": { key: "value", nested: { deep: "data" } } as any,
+ "simple.attr": "simple"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(props["simple.attr"], "Simple attributes should work");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with malformed HTTP status codes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("malformed-status", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ [SEMATTRS_HTTP_STATUS_CODE]: "not-a-number" as any
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle malformed status codes");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with missing parent context",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - Explicitly pass null/undefined parent context
+ const span = this._ai.startSpan("orphan-span", {
+ kind: eOTelSpanKind.CLIENT
+ }, undefined);
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle missing parent");
+ const item = this._trackCalls[0];
+ Assert.ok((item.tags as any)["ai.operation.id"], "Should have operation ID");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span ended multiple times",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("multi-end", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+ const firstCallCount = this._trackCalls.length;
+ span?.end(); // End again
+ const secondCallCount = this._trackCalls.length;
+
+ // Assert
+ Assert.equal(firstCallCount, 1, "First end should generate telemetry");
+ Assert.equal(secondCallCount, 1, "Second end should not generate duplicate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Span with extremely large number of attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const attributes: any = {};
+ for (let i = 0; i < 1000; i++) {
+ attributes["attr." + i] = "value" + i;
+ }
+
+ // Act
+ const span = this._ai.startSpan("many-attrs", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: attributes
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle many attributes");
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(Object.keys(props).length > 0, "Should have some properties");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Zero duration span",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - End span immediately
+ const span = this._ai.startSpan("instant-span", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ const duration = (item.baseData as any).duration;
+ Assert.ok(duration !== undefined, "Should have duration field");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: HTTP dependency with missing URL",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("http-no-url", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_HTTP_METHOD]: "GET",
+ [SEMATTRS_HTTP_STATUS_CODE]: 200
+ // No URL attribute
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle missing URL");
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "Http", "Should still be HTTP type");
+ }
+ });
+
+ this.testCase({
+ name: "Edge Case: Database dependency with missing statement",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("db-no-statement", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_DB_SYSTEM]: "postgresql",
+ [SEMATTRS_DB_NAME]: "testdb"
+ // No statement
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.equal((item.baseData as any).type, "postgresql", "Should have DB type");
+ }
+ });
+ }
+
+ private addCrossBrowserCompatibilityTests(): void {
+ this.testCase({
+ name: "Cross-Browser: Handles performance.now() unavailable",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - Create span when performance.now might not be available
+ const span = this._ai.startSpan("perf-test", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should work without performance.now");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should generate valid telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Handles Date.now() for timing",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("date-timing", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok((item.baseData as any).duration !== undefined, "Should have duration");
+ Assert.ok((item.baseData as any).duration >= 0, "Duration should be non-negative");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: String encoding compatibility",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const testStrings = [
+ "ASCII only",
+ "UTF-8: 你好世界",
+ "Emoji: 🎉🚀💻",
+ "Latin: café résumé",
+ "Mixed: Hello世界🌍"
+ ];
+
+ // Act
+ for (let i = 0; i < testStrings.length; i++) {
+ const span = this._ai.startSpan(testStrings[i], {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.string": testStrings[i]
+ }
+ });
+ span?.end();
+ }
+
+ // Assert
+ Assert.equal(this._trackCalls.length, testStrings.length, "Should handle all encodings");
+ for (let i = 0; i < testStrings.length; i++) {
+ const item = this._trackCalls[i];
+ Assert.ok(item.baseData, "Should have baseData for encoding test " + i);
+ }
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: JSON serialization of attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("json-test", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "number": 123,
+ "string": "test",
+ "boolean": true,
+ "float": 123.456,
+ "negative": -999,
+ "zero": 0
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.equal(typeof props["number"], "number", "Numbers should remain numbers");
+ Assert.equal(typeof props["string"], "string", "Strings should remain strings");
+ Assert.equal(typeof props["boolean"], "boolean", "Booleans should remain booleans");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Large payload handling",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const largeAttributes: any = {};
+ for (let i = 0; i < 100; i++) {
+ largeAttributes["large.attr." + i] = "x".repeat(100);
+ }
+
+ // Act
+ const span = this._ai.startSpan("large-payload", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: largeAttributes
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle large payloads");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Handles undefined vs null attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("null-undefined", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "explicit.null": null as any,
+ "explicit.undefined": undefined as any,
+ "valid.value": "test"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.equal(props["valid.value"], "test", "Valid values should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Whitespace handling in attribute keys",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("whitespace-keys", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "normal.key": "value1",
+ " leading.space": "value2",
+ "trailing.space ": "value3",
+ "has spaces": "value4"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle whitespace in keys");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Number precision and special values",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("number-precision", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "max.safe.integer": Number.MAX_SAFE_INTEGER,
+ "min.safe.integer": Number.MIN_SAFE_INTEGER,
+ "large.float": 1.7976931348623157e+308,
+ "small.float": 5e-324,
+ "infinity": Infinity as any,
+ "neg.infinity": -Infinity as any,
+ "not.a.number": NaN as any
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle special number values");
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.ok(props["max.safe.integer"] !== undefined, "Should handle large integers");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: URL parsing with various formats",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const urls = [
+ "http://example.com",
+ "https://example.com:8080/path",
+ "http://example.com/path?query=value",
+ "https://user:pass@example.com/path",
+ "http://192.168.1.1:3000",
+ "https://[::1]:8080/path"
+ ];
+
+ // Act
+ for (const url of urls) {
+ const span = this._ai.startSpan("url-test", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ [SEMATTRS_HTTP_URL]: url
+ }
+ });
+ span?.end();
+ }
+
+ // Assert
+ Assert.equal(this._trackCalls.length, urls.length, "Should handle all URL formats");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Timestamp handling across timezones",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("timezone-test", {
+ kind: eOTelSpanKind.CLIENT
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ Assert.ok(item.time, "Should have timestamp");
+ const timestamp = new Date(item.time || "").getTime();
+ Assert.ok(timestamp > 0, "Timestamp should be valid");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Memory efficient attribute storage",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - Create many spans to test memory handling
+ for (let i = 0; i < 10; i++) {
+ const span = this._ai.startSpan("memory-test-" + i, {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "iteration": i,
+ "data": "x".repeat(1000)
+ }
+ });
+ span?.end();
+ }
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 10, "Should handle multiple spans");
+ Assert.ok(this._trackCalls[0].baseData, "First span should have data");
+ Assert.ok(this._trackCalls[9].baseData, "Last span should have data");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Concurrent span operations",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spans: any[] = [];
+
+ // Act - Create multiple spans before ending any
+ for (let i = 0; i < 5; i++) {
+ const span = this._ai.startSpan("concurrent-" + i, {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "index": i
+ }
+ });
+ spans.push(span);
+ }
+
+ // End all spans
+ for (const span of spans) {
+ span?.end();
+ }
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 5, "Should handle concurrent spans");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: RegExp in attribute values",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("regexp-test", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "pattern": "/test/gi" as any,
+ "normal": "value"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle RegExp-like values");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Function and Symbol values filtered",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("special-types", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "function": (() => "test") as any,
+ "symbol": Symbol("test") as any,
+ "normal": "value"
+ }
+ });
+ span?.end();
+
+ // Assert
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.equal(props["normal"], "value", "Normal values should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "Cross-Browser: Circular reference handling",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const circular: any = { a: "value" };
+ circular.self = circular;
+
+ // Act
+ const span = this._ai.startSpan("circular-test", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "circular": circular,
+ "normal": "value"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should handle circular references gracefully");
+ const item = this._trackCalls[0];
+ const props = (item.baseData as any).properties || {};
+ Assert.equal(props["normal"], "value", "Normal attributes should still work");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/StartSpan.Tests.ts b/AISKU/Tests/Unit/src/StartSpan.Tests.ts
new file mode 100644
index 000000000..0ba05e737
--- /dev/null
+++ b/AISKU/Tests/Unit/src/StartSpan.Tests.ts
@@ -0,0 +1,301 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { eOTelSpanKind, eOTelSpanStatusCode, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+
+export class StartSpanTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${StartSpanTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+
+ // Track calls to track
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "StartSpanTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: StartSpanTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ // Initialize the SDK
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addTests();
+ }
+
+ private addTests(): void {
+
+ this.testCase({
+ name: "StartSpan: startSpan method should exist on ApplicationInsights instance",
+ test: () => {
+ // Verify that startSpan method exists
+ Assert.ok(this._ai, "ApplicationInsights should be initialized");
+ Assert.ok(typeof this._ai.startSpan === 'function', "startSpan method should exist");
+
+ // Check core initialization
+ Assert.ok(this._ai.core, "Core should be available");
+ const core = this._ai.core;
+ if (core) {
+ // Check if core has startSpan method
+ Assert.ok(typeof core.startSpan === 'function', "Core should have startSpan method");
+
+ // Test basic startSpan call on the core directly after initialization
+ const coreSpan = core.startSpan("debug-core-span");
+ Assert.ok(coreSpan !== null, `Core startSpan returned ${coreSpan} instead of a span object`);
+ }
+
+ // Test basic startSpan call after initialization
+ const span = this._ai.startSpan("debug-span");
+
+ // Should now return a valid span object
+ Assert.ok(span !== null, `startSpan returned ${span} instead of a span object`);
+
+ Assert.ok(typeof span!.isRecording === 'function', "Span should have isRecording method");
+ Assert.ok(typeof span!.end === 'function', "Span should have end method");
+ const isRecording = span!.isRecording();
+ Assert.ok(typeof isRecording === 'boolean', `isRecording should return boolean, got ${typeof isRecording}: ${isRecording}`);
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: Recording span should trigger track when span ends",
+ test: () => {
+ // Clear previous calls
+ this._trackCalls = [];
+
+ // Create a recording span using startSpan
+ const span = this._ai.startSpan("test-recording-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.attribute": "test-value",
+ "operation.type": "http"
+ }
+ });
+
+ Assert.ok(span, "Span should be created");
+
+ // Verify it's a recording span
+ Assert.ok(span!.isRecording(), "Span should be recording");
+
+ // End the span - this should trigger track via the onEnd callback
+ span!.end();
+
+ // Verify that track was called
+ Assert.equal(1, this._trackCalls.length, "track should have been called once for recording span");
+
+ // Add defensive check for the telemetry item
+ Assert.ok(this._trackCalls.length > 0, "Should have at least one track call");
+ const item = this._trackCalls[0];
+ Assert.ok(item, "Telemetry item should exist");
+ Assert.ok(item.name, "Item name should be present");
+ Assert.ok(item.baseData, "Base data should be present");
+
+ Assert.ok(item.baseData.properties, "Custom properties should be present");
+ Assert.equal("test-value", item.baseData.properties["test.attribute"], "Should include span attributes");
+ Assert.equal("http", item.baseData.properties["operation.type"], "Should include all span attributes");
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: Non-recording span should NOT trigger track when span ends",
+ test: () => {
+ // Clear previous calls
+ this._trackCalls = [];
+
+ // NOTE: Currently all spans are recording by default
+ // When the recording: false option is implemented, this test will need to be updated
+ // For now, we'll create a regular span and document the expected behavior
+ const span = this._ai.startSpan("test-would-be-non-recording-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.attribute": "non-recording-value"
+ }
+ });
+
+ Assert.ok(span, "Span should be created");
+
+ // Currently, all spans are recording by default
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.ok(span!.isRecording(), "Span should be recording (default behavior)");
+
+ // End the span - this WILL trigger track since it's recording
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+
+ // Currently expecting 1 call since all spans are recording
+ // When non-recording spans are implemented, this should be 0
+ Assert.equal(1, this._trackCalls.length, "track should be called for recording span (current default behavior)");
+
+ // TODO: Update this test when recording: false option is implemented
+ // The test should then use recording: false and expect 0 track calls
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: Multiple recording spans should each trigger track",
+ test: () => {
+ // Clear previous calls
+ this._trackCalls = [];
+
+ // Create multiple recording spans
+ const span1 = this._ai.startSpan("span-1", {
+ attributes: { "span.number": 1 }
+ });
+ const span2 = this._ai.startSpan("span-2", {
+ attributes: { "span.number": 2 }
+ });
+
+ Assert.ok(span1 && span2, "Both spans should be created");
+
+ // End both spans
+ span1!.end();
+ span2!.end();
+
+ // Should have two track calls
+ Assert.equal(2, this._trackCalls.length, "track should have been called twice");
+
+ // Verify both calls have the correct data
+ const item1 = this._trackCalls.find(item =>
+ item.baseData && item.baseData.properties && item.baseData.name === "span-1");
+ const item2 = this._trackCalls.find(item =>
+ item.baseData && item.baseData.properties && item.baseData.name === "span-2");
+
+ Assert.ok(item1, "Should have item for span-1");
+ Assert.ok(item2, "Should have item for span-2");
+
+ if (item1 && item2) {
+ Assert.equal(1, item1.baseData.properties["span.number"], "First span should have correct attribute");
+ Assert.equal(2, item2.baseData.properties["span.number"], "Second span should have correct attribute");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: Error recording spans should generate telemetry with error status",
+ test: () => {
+ // Clear previous calls
+ this._trackCalls = [];
+
+ // Create an error span
+ const span = this._ai.startSpan("error-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "error": true,
+ "error.message": "Something went wrong"
+ }
+ });
+
+ Assert.ok(span, "Span should be created");
+
+ // Set error status on the span
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Test error occurred"
+ });
+
+ // End the span
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+
+ // Verify track was called
+ Assert.equal(1, this._trackCalls.length, "track should have been called once");
+
+ const item = this._trackCalls[0];
+ Assert.ok(item, "Telemetry item should be present");
+ Assert.ok(item.baseData, "Base data should be present");
+ Assert.ok(item.baseData.properties, "Properties should be present");
+ Assert.equal("error-span", item.baseData.name, "Should include span name");
+
+
+ Assert.ok(item.baseData.properties, "Custom properties should be present");
+ Assert.equal(true, item.baseData.properties["error"], "Should include error attribute");
+ Assert.equal("Something went wrong", item.baseData.properties["error.message"], "Should include error message");
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: startSpan with parent context should work",
+ test: () => {
+ // Clear previous calls
+ this._trackCalls = [];
+
+ // Create span with optional parent context parameter
+ // (We'll pass null for now since we're not testing context propagation yet)
+ const parentContext = null;
+
+ // Create span with parent context
+ const span = this._ai.startSpan("child-span", {
+ attributes: { "has.parent": false }
+ });
+
+ Assert.ok(span, "Span should be created with parent context");
+
+ // End the span
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ span!.end();
+
+ // Verify track was called
+ Assert.equal(1, this._trackCalls.length, "track should have been called once");
+
+ const item = this._trackCalls[0];
+ Assert.ok(item, "Telemetry item should be present");
+ Assert.ok(item.baseData && item.baseData.properties, "Properties should be present");
+ Assert.equal("child-span", item.baseData.name, "Should include span name");
+ Assert.equal(false, item.baseData.properties["has.parent"], "Should include span attributes");
+ }
+ });
+
+ this.testCase({
+ name: "StartSpan: startSpan should return valid span when trace provider is available",
+ test: () => {
+ // After initialization, the trace provider should be available
+ // and startSpan should return a valid span object
+ const span = this._ai.startSpan("test-span");
+
+ // Now that initialization is complete, we should get a valid span
+ Assert.ok(span !== null, "startSpan should return a valid span after initialization");
+ Assert.ok(typeof span === 'object', "Span should be an object");
+
+ Assert.ok(typeof span!.end === 'function', "Span should have end method");
+ Assert.ok(typeof span!.isRecording === 'function', "Span should have isRecording method");
+ span!.end();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/AISKU/Tests/Unit/src/TelemetryItemGeneration.Tests.ts b/AISKU/Tests/Unit/src/TelemetryItemGeneration.Tests.ts
new file mode 100644
index 000000000..9d827180c
--- /dev/null
+++ b/AISKU/Tests/Unit/src/TelemetryItemGeneration.Tests.ts
@@ -0,0 +1,831 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { eOTelSpanKind, eOTelSpanStatusCode, ITelemetryItem } from "@microsoft/applicationinsights-core-js";
+
+export class TelemetryItemGenerationTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${TelemetryItemGenerationTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "TelemetryItemGenerationTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: TelemetryItemGenerationTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addSpanKindTests();
+ this.addStatusCodeTests();
+ this.addAttributeTests();
+ this.addTelemetryItemStructureTests();
+ this.addComplexScenarioTests();
+ }
+
+ private addSpanKindTests(): void {
+ this.testCase({
+ name: "SpanKind: INTERNAL span generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("internal-operation", {
+ kind: eOTelSpanKind.INTERNAL,
+ attributes: { "operation.name": "internal-task" }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate one telemetry item");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.ok(item.baseData.properties, "Should have properties");
+ }
+ });
+
+ this.testCase({
+ name: "SpanKind: CLIENT span generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("client-request", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "GET",
+ "http.url": "https://example.com/api",
+ "custom.attribute": "custom-value"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate one telemetry item");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ // Semantic attributes like http.method are excluded from properties
+ Assert.ok(!item.baseData.properties || !item.baseData.properties["http.method"],
+ "http.method should not be in properties (mapped to baseData)");
+ // Custom attributes should be in properties
+ Assert.equal(item.baseData.properties?.["custom.attribute"], "custom-value",
+ "Custom attributes should be in properties");
+ }
+ });
+
+ this.testCase({
+ name: "SpanKind: SERVER span generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("server-handler", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "http.method": "POST",
+ "http.status_code": 200,
+ "custom.server.id": "server-123"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate one telemetry item");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ // Semantic attributes are excluded from properties
+ Assert.ok(!item.baseData.properties || !item.baseData.properties["http.method"],
+ "http.method should not be in properties (mapped to baseData)");
+ // Custom attributes should be in properties
+ Assert.equal(item.baseData.properties?.["custom.server.id"], "server-123",
+ "Custom attributes should be in properties");
+ }
+ });
+
+ this.testCase({
+ name: "SpanKind: PRODUCER span generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("message-producer", {
+ kind: eOTelSpanKind.PRODUCER,
+ attributes: {
+ "messaging.system": "kafka",
+ "messaging.destination": "orders-topic",
+ "producer.id": "producer-456"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate one telemetry item");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ // messaging.* attributes may or may not be excluded depending on semantic conventions
+ // Custom attributes should be in properties
+ Assert.equal(item.baseData.properties?.["producer.id"], "producer-456",
+ "Custom attributes should be in properties");
+ }
+ });
+
+ this.testCase({
+ name: "SpanKind: CONSUMER span generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("message-consumer", {
+ kind: eOTelSpanKind.CONSUMER,
+ attributes: {
+ "messaging.system": "rabbitmq",
+ "messaging.operation": "receive",
+ "consumer.group": "group-789"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate one telemetry item");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ // messaging.* attributes may or may not be excluded depending on semantic conventions
+ // Custom attributes should be in properties
+ Assert.equal(item.baseData.properties?.["consumer.group"], "group-789",
+ "Custom attributes should be in properties");
+ }
+ });
+
+ this.testCase({
+ name: "SpanKind: all span kinds generate independent telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spanKinds = [
+ eOTelSpanKind.INTERNAL,
+ eOTelSpanKind.CLIENT,
+ eOTelSpanKind.SERVER,
+ eOTelSpanKind.PRODUCER,
+ eOTelSpanKind.CONSUMER
+ ];
+
+ // Act
+ spanKinds.forEach((kind, index) => {
+ const span = this._ai.startSpan(`span-kind-${index}`, { kind });
+ span?.end();
+ });
+
+ // Assert
+ Assert.equal(this._trackCalls.length, spanKinds.length,
+ "Each span kind should generate telemetry");
+ }
+ });
+ }
+
+ private addStatusCodeTests(): void {
+ this.testCase({
+ name: "StatusCode: UNSET status generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("unset-status-span");
+ // Don't set status - defaults to UNSET
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "StatusCode: OK status generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("ok-status-span");
+ span?.setStatus({
+ code: eOTelSpanStatusCode.OK,
+ message: "Operation successful"
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "StatusCode: ERROR status generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("error-status-span");
+ span?.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Operation failed"
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "StatusCode: status with message includes message in telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const errorMessage = "Database connection timeout";
+
+ // Act
+ const span = this._ai.startSpan("status-with-message");
+ span?.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: errorMessage
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ // Note: Implementation may include status message in properties or elsewhere
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData with status information");
+ }
+ });
+
+ this.testCase({
+ name: "StatusCode: changing status before end affects telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("changing-status-span");
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.setStatus({ code: eOTelSpanStatusCode.ERROR, message: "Changed to error" });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ // The final status (ERROR) should be reflected in telemetry
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData with final status");
+ }
+ });
+
+ this.testCase({
+ name: "StatusCode: multiple spans with different statuses",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span1 = this._ai.startSpan("span-ok");
+ span1?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span1?.end();
+
+ const span2 = this._ai.startSpan("span-error");
+ span2?.setStatus({ code: eOTelSpanStatusCode.ERROR });
+ span2?.end();
+
+ const span3 = this._ai.startSpan("span-unset");
+ // No status set
+ span3?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 3, "Should generate telemetry for all spans");
+ }
+ });
+ }
+
+ private addAttributeTests(): void {
+ this.testCase({
+ name: "Attributes: span with no attributes generates telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("no-attributes-span");
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: span with string attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("string-attrs-span", {
+ attributes: {
+ "user.id": "user123",
+ "session.id": "session456",
+ "operation.name": "checkout"
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ // These custom attributes should be in properties
+ Assert.equal(item.baseData.properties["user.id"], "user123",
+ "Should include custom string attributes");
+ Assert.equal(item.baseData.properties["session.id"], "session456",
+ "Should include custom string attributes");
+ // operation.name is a context tag key and gets excluded from properties
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: span with number attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("number-attrs-span", {
+ attributes: {
+ "request.size": 1024,
+ "response.time": 156.78,
+ "retry.count": 3
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ // Custom number attributes should be in properties
+ Assert.equal(item.baseData.properties["request.size"], 1024,
+ "Should include custom number attributes");
+ Assert.equal(item.baseData.properties["response.time"], 156.78,
+ "Should include custom number attributes");
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: span with boolean attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("boolean-attrs-span", {
+ attributes: {
+ "cache.hit": true,
+ "auth.required": false,
+ "retry.enabled": true
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["cache.hit"], true,
+ "Should include boolean attributes");
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: span with mixed type attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("mixed-attrs-span", {
+ attributes: {
+ "string.attr": "value",
+ "number.attr": 42,
+ "boolean.attr": true,
+ "float.attr": 3.14
+ }
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["string.attr"], "value");
+ Assert.equal(item.baseData.properties["number.attr"], 42);
+ Assert.equal(item.baseData.properties["boolean.attr"], true);
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: setAttribute after creation adds to telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("dynamic-attrs-span");
+ span?.setAttribute("initial.attr", "initial");
+ span?.setAttribute("added.later", "later-value");
+ span?.setAttribute("number.added", 999);
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["initial.attr"], "initial");
+ Assert.equal(item.baseData.properties["added.later"], "later-value");
+ Assert.equal(item.baseData.properties["number.added"], 999);
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: setAttributes adds multiple attributes to telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("batch-attrs-span");
+ span?.setAttributes({
+ "batch.attr1": "value1",
+ "batch.attr2": "value2",
+ "batch.attr3": 123
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["batch.attr1"], "value1");
+ Assert.equal(item.baseData.properties["batch.attr2"], "value2");
+ Assert.equal(item.baseData.properties["batch.attr3"], 123);
+ }
+ });
+
+ this.testCase({
+ name: "Attributes: updating attribute value reflects in telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("update-attr-span");
+ span?.setAttribute("status", "pending");
+ span?.setAttribute("status", "in-progress");
+ span?.setAttribute("status", "completed");
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["status"], "completed",
+ "Should reflect final attribute value");
+ }
+ });
+ }
+
+ private addTelemetryItemStructureTests(): void {
+ this.testCase({
+ name: "Structure: telemetry item has required fields",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("structure-test-span");
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+
+ Assert.ok(item.name, "Should have name");
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.ok(item.baseData.properties, "Should have properties");
+ }
+ });
+
+ this.testCase({
+ name: "Structure: span name is in telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const spanName = "my-custom-operation";
+
+ // Act
+ const span = this._ai.startSpan(spanName);
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+
+ // Span name is in baseData.name, not properties.name
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.equal(item.baseData.name, spanName,
+ "Span name should be in baseData.name");
+ }
+ });
+
+ this.testCase({
+ name: "Structure: updated span name reflects in telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const originalName = "original-name";
+ const updatedName = "updated-name";
+
+ // Act
+ const span = this._ai.startSpan(originalName);
+ span?.updateName(updatedName);
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+
+ // Updated span name should be in baseData.name
+ Assert.ok(item.baseData, "Should have baseData");
+ Assert.equal(item.baseData.name, updatedName,
+ "Updated span name should be in baseData.name");
+ }
+ });
+
+ this.testCase({
+ name: "Structure: trace context is in telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("trace-context-span");
+ const spanContext = span?.spanContext();
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+
+ // Telemetry should include trace context information
+ Assert.ok(spanContext, "Span should have context");
+ Assert.ok(spanContext?.traceId, "Should have traceId");
+ Assert.ok(spanContext?.spanId, "Should have spanId");
+ }
+ });
+
+ this.testCase({
+ name: "Structure: multiple spans generate separate telemetry items",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span1 = this._ai.startSpan("span-1");
+ span1?.end();
+
+ const span2 = this._ai.startSpan("span-2");
+ span2?.end();
+
+ const span3 = this._ai.startSpan("span-3");
+ span3?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 3, "Should generate 3 telemetry items");
+
+ // Span names are in baseData.name, not properties.name
+ const names = this._trackCalls.map(item => item.baseData?.name);
+ Assert.ok(names.includes("span-1"), "Should include span-1");
+ Assert.ok(names.includes("span-2"), "Should include span-2");
+ Assert.ok(names.includes("span-3"), "Should include span-3");
+ }
+ });
+ }
+
+ private addComplexScenarioTests(): void {
+ this.testCase({
+ name: "Complex: span with kind, status, and attributes",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("complex-span", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "http.method": "POST",
+ "http.url": "https://api.example.com/users",
+ "http.status_code": 201,
+ "request.id": "req-12345",
+ "user.action": "create"
+ }
+ });
+ span?.setStatus({
+ code: eOTelSpanStatusCode.OK,
+ message: "User created successfully"
+ });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData, "Should have baseData");
+ // Semantic attributes are excluded from properties
+ Assert.ok(!item.baseData.properties || !item.baseData.properties["http.method"],
+ "http.method should not be in properties");
+ // Custom attributes should be in properties
+ Assert.equal(item.baseData.properties?.["request.id"], "req-12345",
+ "Custom attributes should be in properties");
+ Assert.equal(item.baseData.properties?.["user.action"], "create",
+ "Custom attributes should be in properties");
+ }
+ });
+
+ this.testCase({
+ name: "Complex: parent-child spans generate separate telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const parentSpan = this._ai.startSpan("parent-operation");
+ const parentContext = parentSpan?.spanContext();
+
+ const childSpan = this._ai.startSpan("child-operation", undefined, parentContext);
+ childSpan?.end();
+
+ parentSpan?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 2, "Should generate telemetry for both spans");
+
+ // Span names are in baseData.name, not properties.name
+ const names = this._trackCalls.map(item => item.baseData?.name);
+ Assert.ok(names.includes("child-operation"), "Should include child telemetry");
+ Assert.ok(names.includes("parent-operation"), "Should include parent telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "Complex: span with dynamic attributes during execution",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const span = this._ai.startSpan("dynamic-execution-span", {
+ attributes: { "phase": "start" }
+ });
+
+ span?.setAttribute("phase", "processing");
+ span?.setAttribute("items.processed", 50);
+
+ span?.setAttribute("phase", "finalizing");
+ span?.setAttribute("items.processed", 100);
+
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.end();
+
+ // Assert
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ const item = this._trackCalls[0];
+ Assert.ok(item.baseData?.properties, "Should have properties");
+ Assert.equal(item.baseData.properties["phase"], "finalizing",
+ "Should have final phase value");
+ Assert.equal(item.baseData.properties["items.processed"], 100,
+ "Should have final processed count");
+ }
+ });
+
+ this.testCase({
+ name: "Complex: all span kinds with attributes and status",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const testData = [
+ { kind: eOTelSpanKind.INTERNAL, name: "internal-op", attr: "internal-value" },
+ { kind: eOTelSpanKind.CLIENT, name: "client-op", attr: "client-value" },
+ { kind: eOTelSpanKind.SERVER, name: "server-op", attr: "server-value" },
+ { kind: eOTelSpanKind.PRODUCER, name: "producer-op", attr: "producer-value" },
+ { kind: eOTelSpanKind.CONSUMER, name: "consumer-op", attr: "consumer-value" }
+ ];
+
+ // Act
+ testData.forEach(data => {
+ const span = this._ai.startSpan(data.name, {
+ kind: data.kind,
+ attributes: { "operation.type": data.attr }
+ });
+ span?.setStatus({ code: eOTelSpanStatusCode.OK });
+ span?.end();
+ });
+
+ // Assert
+ Assert.equal(this._trackCalls.length, testData.length,
+ "Should generate telemetry for all span types");
+
+ testData.forEach(data => {
+ // Span names are in baseData.name, not properties.name
+ const telemetry = this._trackCalls.find(
+ item => item.baseData?.name === data.name
+ );
+ Assert.ok(telemetry, `Should have telemetry for ${data.name}`);
+ // Custom attributes should be in properties
+ Assert.equal(telemetry?.baseData?.properties?.["operation.type"], data.attr,
+ `Should have correct attributes for ${data.name}`);
+ });
+ }
+ });
+
+ this.testCase({
+ name: "Complex: non-recording spans do not generate telemetry",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act
+ const recordingSpan = this._ai.startSpan("recording-span", { recording: true });
+ recordingSpan?.end();
+
+ const nonRecordingSpan = this._ai.startSpan("non-recording-span", { recording: false });
+ nonRecordingSpan?.end();
+
+ // Assert
+ // Recording span should generate telemetry, non-recording should not
+ // Span names are in baseData.name, not properties.name
+ const recordingTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "recording-span"
+ );
+ const nonRecordingTelemetry = this._trackCalls.find(
+ item => item.baseData?.name === "non-recording-span"
+ );
+
+ Assert.ok(recordingTelemetry, "Recording span should generate telemetry");
+ Assert.ok(!nonRecordingTelemetry, "Non-recording span should not generate telemetry");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts b/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts
index 1e4fc388a..50caf5325 100644
--- a/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts
+++ b/AISKU/Tests/Unit/src/ThrottleSentMessage.tests.ts
@@ -1,6 +1,6 @@
import { ApplicationInsights, ApplicationInsightsContainer, IApplicationInsights, IConfig, IConfiguration, LoggingSeverity, Snippet, _eInternalMessageId } from '../../../src/applicationinsights-web'
import { AITestClass, Assert} from '@microsoft/ai-test-framework';
-import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig } from '@microsoft/applicationinsights-common';
+import { IThrottleInterval, IThrottleLimit, IThrottleMgrConfig } from '@microsoft/applicationinsights-core-js';
import { SinonSpy } from 'sinon';
import { AppInsightsSku } from '../../../src/AISku';
import { createSnippetV5 } from './testSnippetV5';
diff --git a/AISKU/Tests/Unit/src/TraceContext.Tests.ts b/AISKU/Tests/Unit/src/TraceContext.Tests.ts
new file mode 100644
index 000000000..49b75be42
--- /dev/null
+++ b/AISKU/Tests/Unit/src/TraceContext.Tests.ts
@@ -0,0 +1,735 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import { IOTelSpanOptions, eOTelSpanKind, ITelemetryItem, isUndefined, useSpan, isNumber } from "@microsoft/applicationinsights-core-js";
+import { isFunction, objIs } from '@nevware21/ts-utils';
+
+export class TraceContextTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${TraceContextTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "TraceContextTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: TraceContextTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addGetTraceCtxTests();
+ this.addActiveSpanTests();
+ this.addsetActiveSpanTests();
+ this.addIntegrationTests();
+ }
+
+ private addGetTraceCtxTests(): void {
+ this.testCase({
+ name: "getTraceCtx: should return valid trace context after starting a span",
+ test: () => {
+ // Arrange
+ const spanName = "test-span-with-context";
+
+ // Act
+ const span = this._ai.startSpan(spanName);
+ const traceCtx = this._ai.getTraceCtx();
+
+ // Assert
+ Assert.ok(span !== null, "Span should be created");
+ Assert.ok(traceCtx !== null && traceCtx !== undefined, "Should return trace context after starting span");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.ok(traceCtx!.traceId, "Trace context should have traceId");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.equal("", traceCtx!.spanId, "Trace context should not have a spanId (the default SDK initialization)");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.ok(isUndefined(traceCtx!.traceFlags), "Trace context should NOT have have traceFlags");
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ useSpan(this._ai.core, span!, () => {
+ const nestedTraceCtx = this._ai.getTraceCtx();
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Assert.equal(traceCtx!.traceId, nestedTraceCtx!.traceId, "TraceId should be the same within the span context");
+ Assert.equal(span?.spanContext().traceId, nestedTraceCtx?.traceId, "TraceId should match the active span's traceId");
+ Assert.equal(span?.spanContext().spanId, nestedTraceCtx?.spanId, "SpanId should match the active span's spanId");
+ Assert.equal(span?.spanContext().traceFlags, nestedTraceCtx?.traceFlags, "TraceFlags should match the active span's traceFlags");
+ });
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "getTraceCtx: should return trace context matching active span",
+ test: () => {
+ // Arrange
+ const spanName = "context-matching-span";
+
+ // Act
+ const span = this._ai.startSpan(spanName);
+ const traceCtx = this._ai.getTraceCtx();
+ const spanContext = span?.spanContext();
+
+ // Assert
+ Assert.ok(span !== null, "Span should be created");
+ Assert.ok(traceCtx !== null && traceCtx !== undefined, "Trace context should exist");
+ Assert.ok(spanContext !== null && spanContext !== undefined, "Span context should exist");
+
+ Assert.equal(traceCtx.traceId, spanContext.traceId, "Trace context traceId should match span context");
+ Assert.notEqual(traceCtx.spanId, spanContext.spanId, "Trace context spanId should match span context");
+
+ useSpan(this._ai.core, span!, () => {
+ const activeTraceCtx = this._ai.getTraceCtx();
+ Assert.equal(activeTraceCtx?.traceId, spanContext.traceId, "The active traceId should match span context");
+ Assert.equal(activeTraceCtx?.spanId, spanContext.spanId, "The active spanId should match span context");
+ });
+
+ Assert.equal(traceCtx.traceId, spanContext.traceId, "Trace context traceId should match span context");
+ Assert.notEqual(traceCtx.spanId, spanContext.spanId, "Trace context spanId should match span context");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "getTraceCtx: should have valid traceId format (32 hex chars)",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("format-test-span");
+
+ // Act
+ const traceCtx = this._ai.getTraceCtx();
+
+ // Assert
+ Assert.ok(traceCtx !== null && traceCtx !== undefined, "Trace context should exist");
+
+ if (traceCtx && traceCtx.traceId) {
+ Assert.equal(traceCtx.traceId.length, 32, "TraceId should be 32 characters");
+ Assert.ok(/^[0-9a-f]{32}$/i.test(traceCtx.traceId),
+ "TraceId should be 32 hex characters");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "getTraceCtx: should have valid spanId format (16 hex chars)",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("spanid-test-span");
+
+ // Act
+ const traceCtx = this._ai.getTraceCtx();
+
+ // Assert
+ Assert.ok(traceCtx !== null && traceCtx !== undefined, "Trace context should exist");
+
+ if (traceCtx && traceCtx.spanId) {
+ Assert.equal(traceCtx.spanId.length, 16, "SpanId should be 16 characters");
+ Assert.ok(/^[0-9a-f]{16}$/i.test(traceCtx.spanId),
+ "SpanId should be 16 hex characters");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "getTraceCtx: should persist context across multiple calls",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("persistence-span");
+
+ // Act
+ const traceCtx1 = this._ai.getTraceCtx();
+ const traceCtx2 = this._ai.getTraceCtx();
+ const traceCtx3 = this._ai.getTraceCtx();
+
+ // Assert
+ Assert.ok(traceCtx1 !== null && traceCtx1 !== undefined, "First call should return context");
+ Assert.ok(traceCtx2 !== null && traceCtx2 !== undefined, "Second call should return context");
+ Assert.ok(traceCtx3 !== null && traceCtx3 !== undefined, "Third call should return context");
+
+ if (traceCtx1 && traceCtx2 && traceCtx3) {
+ Assert.equal(traceCtx1.traceId, traceCtx2.traceId, "TraceId should be consistent");
+ Assert.equal(traceCtx2.traceId, traceCtx3.traceId, "TraceId should be consistent");
+ Assert.equal(traceCtx1.spanId, traceCtx2.spanId, "SpanId should be consistent");
+ Assert.equal(traceCtx2.spanId, traceCtx3.spanId, "SpanId should be consistent");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "getTraceCtx: should return context for child spans with same traceId",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-span");
+ const parentCtx = this._ai.getTraceCtx();
+
+ // Act - create child span
+ const childSpan = this._ai.startSpan("child-span", undefined, parentCtx || undefined);
+
+ let childCtx;
+ useSpan(this._ai.core, childSpan!, () => {
+ childCtx = this._ai.getTraceCtx();
+ });
+
+ // Assert
+ Assert.ok(parentCtx !== null && parentCtx !== undefined, "Parent context should exist");
+ Assert.ok(childCtx !== null && childCtx !== undefined, "Child context should exist");
+
+ Assert.equal(childCtx.traceId, parentCtx.traceId, "Child span should have same traceId as parent");
+ Assert.notEqual(childCtx.spanId, parentCtx.spanId, "Child span should have different spanId from parent");
+
+ Assert.equal(childSpan?.spanContext().traceId, parentCtx.traceId, "Child span should have same traceId as parent");
+ Assert.notEqual(childSpan?.spanContext().spanId, parentCtx.spanId, "Child span should have different spanId from parent");
+ Assert.equal(childSpan?.spanContext().spanId, childCtx.spanId, "Child spanId should match its context");
+ Assert.equal(childSpan?.spanContext().traceId, childCtx.traceId, "Child traceId should match its context");
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+ }
+
+ private addActiveSpanTests(): void {
+ this.testCase({
+ name: "activeSpan: should return null when no span is active (via trace provider)",
+ test: () => {
+ // Assert
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const activeSpan = this._ai.getActiveSpan();
+ Assert.ok(activeSpan, "Should Always return a non-null span when no span is active");
+ Assert.equal(false, activeSpan.isRecording(), "Returned span should be a non-recording span");
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should return null when createNew is false and no span is active",
+ test: () => {
+ // Act
+ const activeSpan = this._ai.getActiveSpan(false);
+
+ // Assert
+ Assert.equal(activeSpan, null, "Should return null when createNew is false and no active span exists");
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should return existing span when createNew is false and span is active",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("test-span");
+ this._ai.setActiveSpan(span);
+
+ // Act
+ const activeSpan = this._ai.getActiveSpan(false);
+
+ // Assert
+ Assert.ok(activeSpan, "Should return the active span");
+ Assert.equal(activeSpan, span, "Should return the same span object");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should return active span after setActiveSpan (via trace provider)",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("active-span-test");
+
+ // Act
+ this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(activeSpan !== null, "Should return the active span");
+ Assert.equal(activeSpan.name, span.name, "Active span should match the set span");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should return the most recently set active span",
+ test: () => {
+ // Arrange
+ const span1 = this._ai.startSpan("span-1");
+ const span2 = this._ai.startSpan("span-2");
+ const span3 = this._ai.startSpan("span-3");
+
+ // Act & Assert
+ this._ai.setActiveSpan(span1);
+ let activeSpan = this._ai.getActiveSpan();
+ Assert.equal(activeSpan.name, span1.name, "Should return span1 as active");
+
+ this._ai.setActiveSpan(span2);
+ activeSpan = this._ai.getActiveSpan();
+ Assert.equal(activeSpan.name, span2.name, "Should return span2 as active");
+
+ this._ai.setActiveSpan(span3);
+ activeSpan = this._ai.getActiveSpan();
+ Assert.equal(activeSpan.name, span3.name, "Should return span3 as active");
+
+ // Cleanup
+ span1?.end();
+ span2?.end();
+ span3?.end();
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: active span should have valid span context",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("context-check-span");
+
+ // Act
+ this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+ const spanContext = activeSpan.spanContext();
+
+ // Assert
+ Assert.ok(activeSpan !== null, "Active span should exist");
+ Assert.ok(objIs(activeSpan, span), "Active span should match the set span");
+ Assert.ok(spanContext !== null && spanContext !== undefined, "Active span should have valid context");
+
+ Assert.ok(spanContext.traceId, "Should have traceId");
+ Assert.ok(spanContext.spanId, "Should have spanId");
+ Assert.ok(isUndefined(spanContext.traceFlags), "Should have default traceFlags (undefined)");
+ Assert.equal(undefined, spanContext?.traceFlags, "TraceFlags should not have sampled flag set by default");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should work with recording spans",
+ test: () => {
+ // Arrange
+ const options: IOTelSpanOptions = {
+ recording: true,
+ kind: eOTelSpanKind.CLIENT
+ };
+ const span = this._ai.startSpan("recording-span", options);
+
+ // Act
+ this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(activeSpan !== null, "Active span should exist");
+ Assert.ok(activeSpan.isRecording(), "Active span should be recording");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "activeSpan: should work with non-recording spans",
+ test: () => {
+ // Arrange
+ const options: IOTelSpanOptions = {
+ recording: false
+ };
+ const span = this._ai.startSpan("non-recording-span", options);
+
+ // Act
+ this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(activeSpan !== null, "Active span should exist");
+ Assert.ok(!activeSpan.isRecording(), "Active span should not be recording");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+ }
+
+ private addsetActiveSpanTests(): void {
+ this.testCase({
+ name: "setActiveSpan: should set a span as active",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("set-active-test");
+
+ // Act
+ const scope = this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(scope !== null, "Scope should be returned");
+ Assert.equal(scope.span, span, "Scope.span should equal the passed span");
+ Assert.ok(activeSpan !== null, "Active span should be set");
+ Assert.equal(activeSpan, span, "ActiveSpan() should return the same span object");
+ Assert.equal(activeSpan.name, span.name, "Set span should be the active span");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "setActiveSpan: should update getTraceCtx to reflect active span",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("trace-ctx-update-test");
+
+ // Act
+ const scope = this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+ const traceCtx = this._ai.getTraceCtx();
+ const spanContext = span.spanContext();
+
+ // Assert
+ Assert.ok(scope !== null, "Scope should be returned");
+ Assert.equal(scope.span, span, "Scope.span should equal the passed span");
+ Assert.equal(activeSpan, span, "ActiveSpan() should return the same span object");
+ Assert.ok(traceCtx !== null && traceCtx !== undefined,
+ "Trace context should be updated");
+
+ if (traceCtx && spanContext) {
+ Assert.equal(traceCtx.traceId, spanContext.traceId,
+ "Trace context should match active span");
+ Assert.equal(traceCtx.spanId, spanContext.spanId,
+ "Trace context spanId should match active span");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "setActiveSpan: should allow switching between multiple spans",
+ test: () => {
+ // Arrange
+ const span1 = this._ai.startSpan("switch-span-1");
+ const span2 = this._ai.startSpan("switch-span-2");
+
+ // Act & Assert
+ // Set first span as active
+ let scope = this._ai.setActiveSpan(span1);
+ let activeSpan = this._ai.getActiveSpan();
+ Assert.equal(scope.span, span1, "Scope.span should equal span1");
+ Assert.equal(activeSpan, span1, "ActiveSpan() should return span1");
+ Assert.equal(activeSpan.name, span1.name, "First span should be active");
+
+ // Switch to second span
+ scope = this._ai.setActiveSpan(span2);
+ activeSpan = this._ai.getActiveSpan();
+ Assert.equal(scope.span, span2, "Scope.span should equal span2");
+ Assert.equal(activeSpan, span2, "ActiveSpan() should return span2");
+ Assert.equal(activeSpan.name, span2.name, "Second span should be active");
+
+ // Switch back to first span
+ scope = this._ai.setActiveSpan(span1);
+ activeSpan = this._ai.getActiveSpan();
+ Assert.equal(scope.span, span1, "Scope.span should equal span1 again");
+ Assert.equal(activeSpan, span1, "ActiveSpan() should return span1 again");
+ Assert.equal(activeSpan.name, span1.name, "First span should be active again");
+
+ // Cleanup
+ span1?.end();
+ span2?.end();
+ }
+ });
+
+ this.testCase({
+ name: "setActiveSpan: should work with spans of different kinds",
+ test: () => {
+ // Arrange
+ const clientSpan = this._ai.startSpan("client-span", { kind: eOTelSpanKind.CLIENT });
+ const serverSpan = this._ai.startSpan("server-span", { kind: eOTelSpanKind.SERVER });
+
+ // Act & Assert
+ let scope = this._ai.setActiveSpan(clientSpan);
+ let activeSpan = this._ai.getActiveSpan();
+ Assert.equal(scope.span, clientSpan, "Scope.span should equal clientSpan");
+ Assert.equal(activeSpan, clientSpan, "ActiveSpan() should return clientSpan");
+ Assert.equal(activeSpan.kind, eOTelSpanKind.CLIENT,
+ "Client span should be active with correct kind");
+
+ scope = this._ai.setActiveSpan(serverSpan);
+ activeSpan = this._ai.getActiveSpan();
+ Assert.equal(scope.span, serverSpan, "Scope.span should equal serverSpan");
+ Assert.equal(activeSpan, serverSpan, "ActiveSpan() should return serverSpan");
+ Assert.equal(activeSpan.kind, eOTelSpanKind.SERVER,
+ "Server span should be active with correct kind");
+
+ // Cleanup
+ clientSpan?.end();
+ serverSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "setActiveSpan: should work with spans that have attributes",
+ test: () => {
+ // Arrange
+ const attributes = {
+ "http.method": "GET",
+ "http.url": "https://example.com",
+ "custom.attribute": "test-value"
+ };
+ const span = this._ai.startSpan("attributed-span", { attributes });
+
+ // Act
+ const scope = this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(scope !== null, "Scope should be returned");
+ Assert.equal(scope.span, span, "Scope.span should equal the passed span");
+ Assert.ok(activeSpan !== null, "Active span should exist");
+ Assert.equal(activeSpan, span, "ActiveSpan() should return the same span object");
+ Assert.equal(activeSpan.name, span.name, "Span name should match");
+
+ const spanAttributes = activeSpan.attributes;
+ Assert.equal(spanAttributes["http.method"], "GET",
+ "Attributes should be preserved");
+ Assert.equal(spanAttributes["http.url"], "https://example.com",
+ "Attributes should be preserved");
+ Assert.equal(spanAttributes["custom.attribute"], "test-value",
+ "Custom attributes should be preserved");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "setActiveSpan: should handle ended spans",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("ended-span");
+
+ // Act
+ span.end();
+
+ const scope = this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(scope !== null, "Scope should be returned");
+ Assert.equal(scope.span, span, "Scope.span should equal the passed ended span");
+ Assert.ok(activeSpan !== null, "Should be able to set ended span as active");
+ Assert.equal(activeSpan, span, "ActiveSpan() should return the same ended span object");
+ Assert.ok(activeSpan.ended, "Active span should be marked as ended");
+
+ // Cleanup is already done (span.end() called)
+ }
+ });
+ }
+
+ private addIntegrationTests(): void {
+ this.testCase({
+ name: "Integration: getTraceCtx, activeSpan, and setActiveSpan should work together",
+ test: () => {
+ // Arrange
+ const span1 = this._ai.startSpan("integration-span-1");
+ const span2 = this._ai.startSpan("integration-span-2");
+
+ // Act & Assert - Set first span active
+ this._ai.setActiveSpan(span1);
+
+ let activeSpan = this._ai.getActiveSpan();
+ let traceCtx = this._ai.getTraceCtx();
+ let span1Context = span1.spanContext();
+
+ Assert.equal(activeSpan.name, span1.name, "Active span should be span1");
+ Assert.equal(traceCtx?.spanId, span1Context.spanId,
+ "Trace context should match span1");
+
+ // Switch to second span
+ this._ai.setActiveSpan(span2);
+
+ activeSpan = this._ai.getActiveSpan();
+ traceCtx = this._ai.getTraceCtx();
+ let span2Context = span2.spanContext();
+
+ Assert.equal(activeSpan.name, span2.name, "Active span should be span2");
+ Assert.equal(traceCtx?.spanId, span2Context.spanId,
+ "Trace context should match span2");
+
+ // Cleanup
+ span1?.end();
+ span2?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: parent-child span relationship via getTraceCtx",
+ test: () => {
+ // Arrange & Act
+ const parentSpan = this._ai.startSpan("integration-parent");
+
+ this._ai.setActiveSpan(parentSpan);
+ const parentCtx = this._ai.getTraceCtx();
+
+ // Create child span using parent context
+ const childSpan = this._ai.startSpan("integration-child", undefined, parentCtx || undefined);
+ this._ai.setActiveSpan(childSpan);
+
+ const childCtx = this._ai.getTraceCtx();
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(parentCtx !== null && parentCtx !== undefined, "Parent context should exist");
+ Assert.ok(childCtx !== null && childCtx !== undefined, "Child context should exist");
+
+ Assert.equal(childCtx.traceId, parentCtx.traceId, "Child should inherit parent's traceId");
+ Assert.notEqual(childCtx.spanId, parentCtx.spanId, "Child should have different spanId");
+
+ Assert.equal(activeSpan.name, "integration-child", "Active span should be the child span");
+
+ // Cleanup
+ childSpan?.end();
+
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: multiple spans with trace context propagation",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("root-span");
+
+ this._ai.setActiveSpan(rootSpan);
+ const rootCtx = this._ai.getTraceCtx();
+
+ // Create first child
+ const child1Span = this._ai.startSpan("child-1", undefined, rootCtx || undefined);
+ this._ai.setActiveSpan(child1Span);
+ const child1Ctx = this._ai.getTraceCtx();
+
+ // Create second child (sibling to first child)
+ const child2Span = this._ai.startSpan("child-2", undefined, rootCtx || undefined);
+ this._ai.setActiveSpan(child2Span);
+ const child2Ctx = this._ai.getTraceCtx();
+
+ // Assert - all should share the same traceId
+ Assert.equal(child1Ctx.traceId, rootCtx.traceId,
+ "Child 1 should share root traceId");
+ Assert.equal(child2Ctx.traceId, rootCtx.traceId,
+ "Child 2 should share root traceId");
+
+ // But have different spanIds
+ Assert.notEqual(child1Ctx.spanId, rootCtx.spanId,
+ "Child 1 should have different spanId");
+ Assert.notEqual(child2Ctx.spanId, rootCtx.spanId,
+ "Child 2 should have different spanId");
+ Assert.notEqual(child1Ctx.spanId, child2Ctx.spanId,
+ "Siblings should have different spanIds");
+
+ // Cleanup
+ child2Span?.end();
+ child1Span?.end();
+
+ rootSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: trace provider availability check",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+ Assert.equal(this._ai.getTraceProvider(), provider, "Core and AI instance should return same trace provider");
+
+ // Assert
+ Assert.ok(provider !== null && provider !== undefined, "Trace provider should be available");
+
+ Assert.ok(isFunction(provider.createSpan), "Provider should have createSpan method");
+ Assert.ok(isFunction(this._ai.getActiveSpan), "Provider should have activeSpan method");
+ Assert.ok(isFunction(this._ai.setActiveSpan), "Provider should have setActiveSpan method");
+ Assert.ok(isFunction(provider.getProviderId), "Provider should have getProviderId method");
+ Assert.ok(isFunction(provider.isAvailable), "Provider should have isAvailable method");
+ }
+ });
+
+ this.testCase({
+ name: "Integration: trace provider isAvailable should reflect initialization state",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+
+ // Assert
+ const isAvailable = provider.isAvailable();
+ Assert.ok(typeof isAvailable === 'boolean', "isAvailable should return boolean");
+ Assert.ok(isAvailable, "Provider should be available after SDK initialization");
+ }
+ });
+
+ this.testCase({
+ name: "Integration: trace provider should have identifiable providerId",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+
+ // Assert
+ if (provider) {
+ const providerId = provider.getProviderId();
+ Assert.ok(providerId, "Provider should have an ID");
+ Assert.ok(typeof providerId === 'string',
+ "Provider ID should be a string");
+ Assert.ok(providerId.length > 0,
+ "Provider ID should not be empty");
+ }
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/TraceProvider.Tests.ts b/AISKU/Tests/Unit/src/TraceProvider.Tests.ts
new file mode 100644
index 000000000..3e11d09aa
--- /dev/null
+++ b/AISKU/Tests/Unit/src/TraceProvider.Tests.ts
@@ -0,0 +1,716 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import {
+ IReadableSpan, IOTelSpanOptions, eOTelSpanKind, ITraceProvider, ITelemetryItem,
+ isFunction
+} from "@microsoft/applicationinsights-core-js";
+
+export class TraceProviderTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${TraceProviderTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "TraceProviderTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: TraceProviderTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addProviderAvailabilityTests();
+ this.addGetProviderIdTests();
+ this.addIsAvailableTests();
+ this.addCreateSpanTests();
+ this.addProviderIntegrationTests();
+ }
+
+ private addProviderAvailabilityTests(): void {
+ this.testCase({
+ name: "TraceProvider: getTraceProvider should return provider instance",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+
+ // Assert
+ Assert.ok(provider !== null && provider !== undefined,
+ "Should return a trace provider instance");
+ Assert.ok(typeof provider === 'object',
+ "Provider should be an object");
+ }
+ });
+
+ this.testCase({
+ name: "TraceProvider: provider should have all required methods",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+ Assert.equal(this._ai.getTraceProvider(), provider, "Core and AI instance should return same trace provider");
+
+ // Assert
+ Assert.ok(provider, "Provider should exist");
+ if (provider) {
+ Assert.ok(isFunction(provider.createSpan), "Should have createSpan method");
+ Assert.ok(isFunction(this._ai.getActiveSpan), "Should have activeSpan method");
+ Assert.ok(isFunction(this._ai.setActiveSpan), "Should have setActiveSpan method");
+ Assert.ok(isFunction(provider.getProviderId), "Should have getProviderId method");
+ Assert.ok(isFunction(provider.isAvailable), "Should have isAvailable method");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "TraceProvider: provider should be available after SDK initialization",
+ test: () => {
+ // Act
+ const provider = this._ai.core.getTraceProvider();
+
+ // Assert
+ Assert.ok(provider !== null, "Provider should not be null");
+ Assert.ok(provider !== undefined, "Provider should not be undefined");
+ }
+ });
+
+ this.testCase({
+ name: "TraceProvider: multiple calls to getTraceProvider should return same provider",
+ test: () => {
+ // Act
+ const provider1 = this._ai.core.getTraceProvider();
+ const provider2 = this._ai.core.getTraceProvider();
+ const provider3 = this._ai.core.getTraceProvider();
+
+ // Assert
+ Assert.ok(provider1 === provider2,
+ "First and second calls should return same provider");
+ Assert.ok(provider2 === provider3,
+ "Second and third calls should return same provider");
+ }
+ });
+ }
+
+ private addGetProviderIdTests(): void {
+ this.testCase({
+ name: "getProviderId: should return a string identifier",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const providerId = provider?.getProviderId();
+
+ // Assert
+ Assert.ok(providerId !== null && providerId !== undefined,
+ "Provider ID should not be null or undefined");
+ Assert.ok(typeof providerId === 'string',
+ "Provider ID should be a string");
+ }
+ });
+
+ this.testCase({
+ name: "getProviderId: should return non-empty string",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const providerId = provider?.getProviderId();
+
+ // Assert
+ if (providerId) {
+ Assert.ok(providerId.length > 0,
+ "Provider ID should not be empty");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "getProviderId: should return consistent ID across multiple calls",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const providerId1 = provider?.getProviderId();
+ const providerId2 = provider?.getProviderId();
+ const providerId3 = provider?.getProviderId();
+
+ // Assert
+ Assert.equal(providerId1, providerId2,
+ "Provider ID should be consistent across calls");
+ Assert.equal(providerId2, providerId3,
+ "Provider ID should be consistent across calls");
+ }
+ });
+
+ this.testCase({
+ name: "getProviderId: should return identifiable name",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const providerId = provider?.getProviderId();
+
+ // Assert
+ Assert.ok(providerId, "Provider ID should exist");
+ if (providerId) {
+ // Provider ID should be a meaningful identifier, not just random characters
+ Assert.ok(providerId.length > 2,
+ "Provider ID should be more than 2 characters");
+ }
+ }
+ });
+ }
+
+ private addIsAvailableTests(): void {
+ this.testCase({
+ name: "isAvailable: should return boolean value",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const isAvailable = provider?.isAvailable();
+
+ // Assert
+ Assert.ok(typeof isAvailable === 'boolean',
+ "isAvailable should return a boolean");
+ }
+ });
+
+ this.testCase({
+ name: "isAvailable: should return true after SDK initialization",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const isAvailable = provider?.isAvailable();
+
+ // Assert
+ Assert.ok(isAvailable === true,
+ "Provider should be available after SDK initialization");
+ }
+ });
+
+ this.testCase({
+ name: "isAvailable: should be consistent across multiple calls",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const isAvailable1 = provider?.isAvailable();
+ const isAvailable2 = provider?.isAvailable();
+ const isAvailable3 = provider?.isAvailable();
+
+ // Assert
+ Assert.equal(isAvailable1, isAvailable2,
+ "Availability should be consistent");
+ Assert.equal(isAvailable2, isAvailable3,
+ "Availability should be consistent");
+ }
+ });
+
+ this.testCase({
+ name: "isAvailable: available provider should allow span creation",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ const isAvailable = provider?.isAvailable();
+
+ // Act
+ let canCreateSpan = false;
+ if (provider && isAvailable) {
+ const span = provider.createSpan("availability-test-span");
+ canCreateSpan = span !== null && span !== undefined;
+ span?.end();
+ }
+
+ // Assert
+ Assert.ok(isAvailable, "Provider should be available");
+ Assert.ok(canCreateSpan,
+ "Available provider should allow span creation");
+ }
+ });
+
+ this.testCase({
+ name: "isAvailable: should reflect provider initialization state",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // After full SDK initialization, provider should be available
+ Assert.ok(provider !== null, "Provider should not be null");
+
+ // Act
+ const isAvailable = provider?.isAvailable();
+
+ Assert.ok(isAvailable !== undefined, "isAvailable should not be undefined");
+
+ // Assert
+
+ Assert.ok(isFunction(provider.createSpan), "Available provider should have createSpan");
+ Assert.ok(isFunction(provider.getProviderId), "Available provider should have getProviderId");
+ Assert.ok(isFunction(provider.isAvailable), "Available provider should have isAvailable");
+ Assert.ok(isFunction(this._ai.getActiveSpan), "Available provider should have activeSpan");
+ Assert.ok(isFunction(this._ai.setActiveSpan), "Available provider should have setActiveSpan");
+ Assert.ok(isFunction(this._ai.core.getActiveSpan), "Available core should have activeSpan");
+ Assert.ok(isFunction(this._ai.core.setActiveSpan), "Available core should have setActiveSpan");
+ }
+ });
+ }
+
+ private addCreateSpanTests(): void {
+ this.testCase({
+ name: "Provider createSpan: should create valid span",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ const spanName = "provider-create-span-test";
+
+ // Act
+ let span: IReadableSpan | null = null;
+ if (provider) {
+ span = provider.createSpan(spanName);
+ }
+
+ // Assert
+ Assert.ok(span !== null && span !== undefined,
+ "Provider should create a span");
+ if (span) {
+ Assert.equal(span.name, spanName, "Span name should match");
+ Assert.ok(typeof span.isRecording === 'function',
+ "Span should have isRecording method");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Provider createSpan: should create span with options",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ const spanName = "provider-span-with-options";
+ const options: IOTelSpanOptions = {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "test.attribute": "value"
+ }
+ };
+
+ // Act
+ let span: IReadableSpan | null = null;
+ if (provider) {
+ span = provider.createSpan(spanName, options);
+ }
+
+ // Assert
+ Assert.ok(span !== null, "Provider should create span with options");
+ if (span) {
+ Assert.equal(span.kind, eOTelSpanKind.CLIENT,
+ "Span kind should match options");
+ Assert.ok(span.attributes["test.attribute"] === "value",
+ "Span attributes should be set");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Provider createSpan: should create span with parent context",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ let parentSpan: IReadableSpan | null = null;
+ let childSpan: IReadableSpan | null = null;
+
+ if (provider) {
+ parentSpan = provider.createSpan("parent-span");
+ const parentCtx = parentSpan.spanContext();
+
+ // Act
+ childSpan = provider.createSpan("child-span", undefined, parentCtx);
+
+ // Assert
+ Assert.ok(childSpan !== null, "Child span should be created");
+ if (childSpan && parentSpan) {
+ const childCtx = childSpan.spanContext();
+ Assert.equal(childCtx.traceId, parentCtx.traceId,
+ "Child should inherit parent traceId");
+ Assert.notEqual(childCtx.spanId, parentCtx.spanId,
+ "Child should have different spanId");
+ }
+ }
+
+ // Cleanup
+ childSpan?.end();
+ parentSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Provider createSpan: should create multiple independent spans",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ let span1: IReadableSpan | null = null;
+ let span2: IReadableSpan | null = null;
+ let span3: IReadableSpan | null = null;
+
+ if (provider) {
+ span1 = provider.createSpan("span-1");
+ span2 = provider.createSpan("span-2");
+ span3 = provider.createSpan("span-3");
+ }
+
+ // Assert
+ Assert.ok(span1 !== null, "First span should be created");
+ Assert.ok(span2 !== null, "Second span should be created");
+ Assert.ok(span3 !== null, "Third span should be created");
+
+ if (span1 && span2 && span3) {
+ const ctx1 = span1.spanContext();
+ const ctx2 = span2.spanContext();
+ const ctx3 = span3.spanContext();
+
+ Assert.notEqual(ctx1.spanId, ctx2.spanId,
+ "Spans should have different spanIds");
+ Assert.notEqual(ctx2.spanId, ctx3.spanId,
+ "Spans should have different spanIds");
+ Assert.notEqual(ctx1.spanId, ctx3.spanId,
+ "Spans should have different spanIds");
+ }
+
+ // Cleanup
+ span1?.end();
+ span2?.end();
+ span3?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Provider createSpan: should create recording spans by default",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ let span: IReadableSpan | null = null;
+ if (provider) {
+ span = provider.createSpan("recording-test");
+ }
+
+ // Assert
+ Assert.ok(span !== null, "Span should be created");
+ if (span) {
+ Assert.ok(span.isRecording(),
+ "Span should be recording by default");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Provider createSpan: should respect recording option when false",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ const options: IOTelSpanOptions = {
+ recording: false
+ };
+
+ // Act
+ let span: IReadableSpan | null = null;
+ if (provider) {
+ span = provider.createSpan("non-recording-test", options);
+ }
+
+ // Assert
+ Assert.ok(span !== null, "Span should be created");
+ if (span) {
+ Assert.ok(!span.isRecording(),
+ "Span should not be recording when options.recording is false");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+ }
+
+ private addProviderIntegrationTests(): void {
+ this.testCase({
+ name: "Integration: provider operations should work with SDK instance",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act - Create span via provider
+ let providerSpan: IReadableSpan | null = null;
+ if (provider) {
+ providerSpan = provider.createSpan("provider-integration-span");
+ }
+
+ // Create span via SDK
+ const sdkSpan = this._ai.startSpan("sdk-integration-span");
+
+ // Assert
+ Assert.ok(providerSpan !== null,
+ "Provider should create span successfully");
+ Assert.ok(sdkSpan !== null,
+ "SDK should create span successfully");
+
+ if (providerSpan && sdkSpan) {
+ // Both spans should have valid contexts
+ const providerCtx = providerSpan.spanContext();
+ const sdkCtx = sdkSpan.spanContext();
+
+ Assert.ok(providerCtx.traceId, "Provider span should have traceId");
+ Assert.ok(providerCtx.spanId, "Provider span should have spanId");
+ Assert.ok(sdkCtx.traceId, "SDK span should have traceId");
+ Assert.ok(sdkCtx.spanId, "SDK span should have spanId");
+ }
+
+ // Cleanup
+ providerSpan?.end();
+ sdkSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider activeSpan and setActiveSpan work together",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ let span: IReadableSpan | null = null;
+ if (provider) {
+ span = provider.createSpan("active-integration-span");
+ this._ai.setActiveSpan(span);
+ const activeSpan = this._ai.getActiveSpan();
+
+ // Assert
+ Assert.ok(activeSpan !== null, "Active span should be retrievable");
+ Assert.equal(activeSpan.name, span.name,
+ "Active span should match the set span");
+ Assert.equal(activeSpan, this._ai.core.getActiveSpan(), "Active span from core should match active span from SDK");
+ }
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider availability affects span creation",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const isAvailable = provider?.isAvailable();
+ let canCreateSpan = false;
+
+ if (provider) {
+ try {
+ const span = provider.createSpan("availability-integration-test");
+ canCreateSpan = span !== null;
+ span?.end();
+ } catch (e) {
+ canCreateSpan = false;
+ }
+ }
+
+ // Assert
+ if (isAvailable) {
+ Assert.ok(canCreateSpan,
+ "Available provider should successfully create spans");
+ } else {
+ // If provider is not available, we should handle it gracefully
+ Assert.ok(!canCreateSpan || canCreateSpan,
+ "Provider availability state should be consistent with span creation");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider ID is consistent with trace operations",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ const providerId = provider?.getProviderId();
+ let span: IReadableSpan | null = null;
+
+ if (provider) {
+ span = provider.createSpan("provider-id-integration");
+ }
+
+ // Assert
+ Assert.ok(providerId, "Provider should have an ID");
+ Assert.ok(span !== null,
+ "Provider with ID should be able to create spans");
+
+ // Cleanup
+ span?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider methods are callable without errors",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act & Assert - All methods should be callable
+ Assert.ok(provider !== null, "Provider should exist");
+
+ if (provider) {
+ // Test getProviderId
+ Assert.doesNotThrow(() => {
+ const id = provider.getProviderId();
+ Assert.ok(typeof id === 'string', "getProviderId should return string");
+ }, "getProviderId should not throw");
+
+ // Test isAvailable
+ Assert.doesNotThrow(() => {
+ const available = provider.isAvailable();
+ Assert.ok(typeof available === 'boolean',
+ "isAvailable should return boolean");
+ }, "isAvailable should not throw");
+
+ // Test createSpan
+ Assert.doesNotThrow(() => {
+ const span = provider.createSpan("error-test-span");
+ Assert.ok(span !== null, "createSpan should return span");
+ span?.end();
+ }, "createSpan should not throw");
+
+ // Test activeSpan
+ Assert.doesNotThrow(() => {
+ const active = this._ai.getActiveSpan();
+ // Can be null, that's ok
+ Assert.ok(active === null || typeof active === 'object',
+ "activeSpan should return null or span object");
+ }, "activeSpan should not throw");
+
+ const span = provider.createSpan("set-active-error-test");
+
+ // Test setActiveSpan
+ Assert.doesNotThrow(() => {
+ this._ai.setActiveSpan(span);
+ span?.end();
+ }, "setActiveSpan should not throw");
+
+ // Test setActiveSpan
+ Assert.doesNotThrow(() => {
+ this._ai.setActiveSpan(span);
+ }, "setActiveSpan should not throw when the span has already ended");
+
+ // Test setActiveSpan
+ Assert.doesNotThrow(() => {
+ span?.end();
+ }, "ending an already ended span should not throw");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider supports root span creation",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+
+ // Act
+ let rootSpan: IReadableSpan | null = null;
+ if (provider) {
+ rootSpan = provider.createSpan("root-span", { root: true });
+ }
+
+ // Assert
+ Assert.ok(rootSpan !== null, "Root span should be created");
+ if (rootSpan) {
+ const ctx = rootSpan.spanContext();
+ Assert.ok(ctx.traceId, "Root span should have traceId");
+ Assert.ok(ctx.spanId, "Root span should have spanId");
+ }
+
+ // Cleanup
+ rootSpan?.end();
+ }
+ });
+
+ this.testCase({
+ name: "Integration: provider supports different span kinds",
+ test: () => {
+ // Arrange
+ const provider = this._ai.core.getTraceProvider();
+ const spanKinds = [
+ eOTelSpanKind.INTERNAL,
+ eOTelSpanKind.SERVER,
+ eOTelSpanKind.CLIENT,
+ eOTelSpanKind.PRODUCER,
+ eOTelSpanKind.CONSUMER
+ ];
+
+ // Act & Assert
+ if (provider) {
+ spanKinds.forEach(kind => {
+ const span = provider.createSpan(`span-kind-${kind}`, { kind });
+ Assert.ok(span !== null, `Span with kind ${kind} should be created`);
+ Assert.equal(span?.kind, kind,
+ `Span should have kind ${kind}`);
+ span?.end();
+ });
+ }
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/TraceSuppression.Tests.ts b/AISKU/Tests/Unit/src/TraceSuppression.Tests.ts
new file mode 100644
index 000000000..820f5a08a
--- /dev/null
+++ b/AISKU/Tests/Unit/src/TraceSuppression.Tests.ts
@@ -0,0 +1,706 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import { eOTelSpanKind, ITelemetryItem, suppressTracing, unsuppressTracing, isTracingSuppressed } from "@microsoft/applicationinsights-core-js";
+
+
+function _createAndInitializeSDK(connectionString: string): ApplicationInsights {
+ let newInst = new ApplicationInsights({
+ config: {
+ connectionString: connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ // Initialize the SDK
+ newInst.loadAppInsights();
+
+ return newInst
+}
+
+export class TraceSuppressionTests extends AITestClass {
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${TraceSuppressionTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+
+ // Track calls to track for validation
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "TraceSuppressionTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+ this._ai = _createAndInitializeSDK(TraceSuppressionTests._connectionString);
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error("Failed to initialize TraceSuppressionTests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addTests();
+ }
+
+ private addTests(): void {
+
+ this.testCase({
+ name: "TraceSuppression: new SDK instance should have tracing enabled by default and state should not leak between instances",
+ test: () => {
+ // Step 1: Verify first instance has tracing enabled by default
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Instance 1: Tracing should NOT be suppressed in new instance");
+ Assert.ok(!isTracingSuppressed(this._ai.core.config), "Instance 1: Tracing should NOT be suppressed on config");
+ Assert.ok(!isTracingSuppressed(this._ai.otelApi), "Instance 1: Tracing should NOT be suppressed on otelApi");
+
+ // Verify that spans can record by default
+ const span1 = this._ai.startSpan("default-span");
+ Assert.ok(span1, "Instance 1: Span should be created");
+ Assert.ok(span1!.isRecording(), "Instance 1: Span should be recording by default");
+ span1!.end();
+ Assert.equal(this._trackCalls.length, 1, "Instance 1: Telemetry should be tracked by default");
+
+ // Step 2: Suppress tracing on this instance
+ suppressTracing(this._ai.core);
+ Assert.ok(isTracingSuppressed(this._ai.core), "Instance 1: Tracing should be suppressed after suppressTracing()");
+
+ // Verify suppression works - span still reports isRecording()=true but doesn't send telemetry
+ const span2 = this._ai.startSpan("suppressed-span");
+ Assert.ok(!span2!.isRecording(), "Instance 2: Span reports isRecording()=false when suppressed");
+ span2!.end();
+ Assert.equal(this._trackCalls.length, 1, "Instance 1: No additional telemetry when suppressed");
+
+ // Step 3: Clean up first instance and create a new instance
+ this._ai.unload(false);
+
+ this._ai = _createAndInitializeSDK(TraceSuppressionTests._connectionString);
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ // Step 4: Verify new instance has tracing enabled by default (not inheriting suppressed state)
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Instance 2: Tracing should NOT be suppressed in new instance");
+ Assert.ok(!isTracingSuppressed(this._ai.core.config), "Instance 2: Tracing should NOT be suppressed on config");
+ Assert.ok(!isTracingSuppressed(this._ai.otelApi), "Instance 2: Tracing should NOT be suppressed on otelApi");
+
+ // Verify that spans can record in the new instance
+ const span3 = this._ai.startSpan("new-instance-span");
+ Assert.ok(span3, "Instance 2: Span should be created");
+ Assert.ok(span3!.isRecording(), "Instance 2: Span should be recording by default (state should not leak)");
+ span3!.end();
+ Assert.equal(this._trackCalls.length, 2, "Instance 2: Telemetry should be tracked in new instance");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing should be available as exported function",
+ test: () => {
+ // Verify that suppressTracing functions are available as imports
+ Assert.ok(typeof suppressTracing === "function", "suppressTracing should be available as exported function");
+ Assert.ok(typeof unsuppressTracing === "function", "unsuppressTracing should be available as exported function");
+ Assert.ok(typeof isTracingSuppressed === "function", "isTracingSuppressed should be available as exported function");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing on core should prevent span recording",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Tracing should not be suppressed initially");
+
+ // Act - suppress tracing
+ suppressTracing(this._ai.core);
+ Assert.ok(isTracingSuppressed(this._ai.core), "Tracing should be suppressed after calling suppressTracing");
+
+ // Create span while tracing is suppressed
+ const span = this._ai.startSpan("suppressed-span", {
+ kind: eOTelSpanKind.INTERNAL,
+ attributes: {
+ "test.suppressed": true
+ }
+ });
+
+ // Assert
+ Assert.ok(span, "Span should still be created");
+ Assert.ok(!span!.isRecording(), "Span reports isRecording()=false");
+
+ // End the span - should not generate telemetry
+ span!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry should be tracked when tracing is suppressed");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: unsuppressTracing should restore span recording",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+ Assert.ok(isTracingSuppressed(this._ai.core), "Tracing should be suppressed");
+
+ // Create span while suppressed - still reports isRecording()=true but won't send telemetry
+ const suppressedSpan = this._ai.startSpan("suppressed-span");
+ Assert.ok(!suppressedSpan!.isRecording(), "Span reports isRecording()=false even when suppressed");
+ suppressedSpan!.end();
+
+ // Act - unsuppress tracing
+ unsuppressTracing(this._ai.core);
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Tracing should not be suppressed after unsuppressTracing");
+
+ // Create new span after unsuppressing
+ const recordingSpan = this._ai.startSpan("recording-span", {
+ attributes: {
+ "test.recording": true
+ }
+ });
+
+ // Assert
+ Assert.ok(recordingSpan, "Span should be created");
+ Assert.ok(recordingSpan!.isRecording(), "Span should be recording after unsuppressing");
+
+ // End the span - should generate telemetry
+ recordingSpan!.end();
+
+ Assert.equal(this._trackCalls.length, 1, "Telemetry should be tracked after unsuppressing");
+ Assert.equal(this._trackCalls[0].baseData?.name, "recording-span", "Tracked span should have correct name");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing on config should prevent span recording",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Suppress via config object
+ suppressTracing(this._ai.core.config);
+ Assert.ok(isTracingSuppressed(this._ai.core.config), "Tracing should be suppressed on config");
+ Assert.ok(isTracingSuppressed(this._ai.core), "Tracing should be suppressed on core");
+
+ // Act - create span
+ const span = this._ai.startSpan("config-suppressed-span");
+
+ // Assert
+ Assert.ok(span, "Span should be created");
+ Assert.ok(!span!.isRecording(), "Span reports isRecording()=false");
+ span!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry when suppressed via config");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: multiple startSpan calls while suppressed should all create non-recording spans",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+
+ // Act - create multiple spans
+ const span1 = this._ai.startSpan("span-1");
+ const span2 = this._ai.startSpan("span-2", { kind: eOTelSpanKind.CLIENT });
+ const span3 = this._ai.startSpan("span-3", { kind: eOTelSpanKind.SERVER });
+
+ // Assert - spans still report isRecording()=true, suppression only affects telemetry output
+ Assert.ok(!span1!.isRecording(), "Span 1 reports isRecording()=false");
+ Assert.ok(!span2!.isRecording(), "Span 2 reports isRecording()=false");
+ Assert.ok(!span3!.isRecording(), "Span 3 reports isRecording()=false");
+
+ // All spans should still be valid and support operations
+ span1!.setAttribute("test", "value1");
+ span2!.setStatus({ code: 0 });
+ span3!.updateName("updated-span-3");
+
+ span1!.end();
+ span2!.end();
+ span3!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry should be generated for any suppressed span");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: parent-child span hierarchy with suppression",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+
+ // Act - create parent and child spans while suppressed
+ const parentSpan = this._ai.startSpan("parent-span", {
+ kind: eOTelSpanKind.SERVER
+ });
+ Assert.ok(!parentSpan!.isRecording(), "Parent span reports isRecording()=false");
+
+ const childSpan = this._ai.startSpan("child-span", {
+ kind: eOTelSpanKind.INTERNAL
+ });
+ Assert.ok(!childSpan!.isRecording(), "Child span reports isRecording()=false");
+
+ // Verify parent-child relationship still established
+ const childContext = childSpan!.spanContext();
+ const parentContext = parentSpan!.spanContext();
+ Assert.equal(childContext.traceId, parentContext.traceId, "Child should share traceId with parent");
+ Assert.notEqual(childContext.spanId, parentContext.spanId, "Child should have different spanId");
+
+ childSpan!.end();
+ parentSpan!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry for suppressed hierarchy");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: toggle suppression during span lifecycle",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Create recording span
+ const span1 = this._ai.startSpan("recording-span");
+ Assert.ok(span1!.isRecording(), "Span should be recording initially");
+
+ // Suppress tracing mid-lifecycle
+ suppressTracing(this._ai.core);
+
+ // Create new span while suppressed
+ const span2 = this._ai.startSpan("suppressed-span");
+ Assert.ok(!span2!.isRecording(), "Span reports isRecording()=false when suppressed");
+
+ // End both spans
+ span1!.end(); // Was recording but tracing has been suppressed before it ends
+ span2!.end(); // Was not recording
+
+ // Verify telemetry
+ Assert.equal(this._trackCalls.length, 0, "Only the recording span should generate telemetry");
+
+ // Unsuppress and create another span
+ unsuppressTracing(this._ai.core);
+ const span3 = this._ai.startSpan("restored-span");
+ Assert.ok(span3!.isRecording(), "New span should be recording after unsuppressing");
+ span3!.end();
+
+ Assert.equal(this._trackCalls.length, 1, "Restored span should generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: toggle suppression during span lifecycle",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Create recording span
+ const span1 = this._ai.startSpan("recording-span");
+ Assert.ok(span1!.isRecording(), "Span should be recording initially");
+
+ // Suppress tracing mid-lifecycle
+ suppressTracing(this._ai.core);
+
+ // Create new span while suppressed
+ const span2 = this._ai.startSpan("suppressed-span");
+ Assert.ok(!span2!.isRecording(), "Span reports isRecording()=false when suppressed");
+
+ // Unsuppress and create another span
+ unsuppressTracing(this._ai.core);
+
+ // End both spans
+ span1!.end(); // Was recording
+ span2!.end(); // Was not recording as tracing was suppressed when created
+
+ // Verify telemetry
+ Assert.equal(this._trackCalls.length, 1, "Only the recording span should generate telemetry");
+ Assert.equal(this._trackCalls[0].baseData?.name, "recording-span", "Recording span telemetry");
+
+ const span3 = this._ai.startSpan("restored-span");
+ Assert.ok(span3!.isRecording(), "New span should be recording after unsuppressing");
+ span3!.end();
+
+ Assert.equal(this._trackCalls.length, 2, "Restored span should generate telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing should affect all span kinds",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+
+ // Act - create spans of all kinds
+ const internalSpan = this._ai.startSpan("internal", { kind: eOTelSpanKind.INTERNAL });
+ const clientSpan = this._ai.startSpan("client", { kind: eOTelSpanKind.CLIENT });
+ const serverSpan = this._ai.startSpan("server", { kind: eOTelSpanKind.SERVER });
+ const producerSpan = this._ai.startSpan("producer", { kind: eOTelSpanKind.PRODUCER });
+ const consumerSpan = this._ai.startSpan("consumer", { kind: eOTelSpanKind.CONSUMER });
+
+ // Assert - all spans still report isRecording()=true, suppression only prevents telemetry output
+ Assert.ok(!internalSpan!.isRecording(), "INTERNAL span reports isRecording()=false");
+ Assert.ok(!clientSpan!.isRecording(), "CLIENT span reports isRecording()=false");
+ Assert.ok(!serverSpan!.isRecording(), "SERVER span reports isRecording()=false");
+ Assert.ok(!producerSpan!.isRecording(), "PRODUCER span reports isRecording()=false");
+ Assert.ok(!consumerSpan!.isRecording(), "CONSUMER span reports isRecording()=false");
+
+ // End all spans
+ internalSpan!.end();
+ clientSpan!.end();
+ serverSpan!.end();
+ producerSpan!.end();
+ consumerSpan!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry for any span kind when suppressed");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: span operations should still work when tracing is suppressed",
+ test: () => {
+ // Arrange
+ suppressTracing(this._ai.core);
+ const span = this._ai.startSpan("suppressed-span");
+ Assert.ok(!span!.isRecording(), "Span reports isRecording()=false");
+
+ // Act - perform various span operations
+ span!.setAttribute("string-attr", "value");
+ span!.setAttribute("number-attr", 42);
+ span!.setAttribute("boolean-attr", true);
+
+ span!.setAttributes({
+ "batch-1": "test1",
+ "batch-2": 123
+ });
+
+ span!.setStatus({
+ code: 0,
+ message: "Test status"
+ });
+
+ span!.updateName("updated-name");
+
+ span!.recordException(new Error("Test exception"));
+
+ // Assert - operations should not throw
+ Assert.ok(true, "All operations completed without throwing");
+
+ // Verify span properties
+ Assert.equal(span!.name, "updated-name", "Name should be updated");
+ Assert.ok(!span!.ended, "Span should not be ended yet");
+
+ span!.end();
+ Assert.ok(span!.ended, "Span should be ended");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: isTracingSuppressed should return false when not suppressed",
+ test: () => {
+ // Ensure no suppression
+ unsuppressTracing(this._ai.core);
+
+ // Assert
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Should return false when not suppressed");
+ Assert.ok(!isTracingSuppressed(this._ai.core.config), "Config should also not be suppressed");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing should return the same context",
+ test: () => {
+ // Act
+ const returnedCore = suppressTracing(this._ai.core);
+ const returnedConfig = suppressTracing(this._ai.core.config);
+
+ // Assert
+ Assert.equal(returnedCore, this._ai.core, "suppressTracing should return the same core instance");
+ Assert.equal(returnedConfig, this._ai.core.config, "suppressTracing should return the same config instance");
+ Assert.ok(isTracingSuppressed(returnedCore), "Returned core should have suppression enabled");
+ Assert.ok(isTracingSuppressed(returnedConfig), "Returned config should have suppression enabled");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: unsuppressTracing should return the same context",
+ test: () => {
+ // Arrange
+ suppressTracing(this._ai.core);
+
+ // Act
+ const returnedCore = unsuppressTracing(this._ai.core);
+ const returnedConfig = unsuppressTracing(this._ai.core.config);
+
+ // Assert
+ Assert.equal(returnedCore, this._ai.core, "unsuppressTracing should return the same core instance");
+ Assert.equal(returnedConfig, this._ai.core.config, "unsuppressTracing should return the same config instance");
+ Assert.ok(!isTracingSuppressed(returnedCore), "Returned core should have suppression disabled");
+ Assert.ok(!isTracingSuppressed(returnedConfig), "Returned config should have suppression disabled");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppression state should persist across multiple checks",
+ test: () => {
+ // Initial state
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Initially not suppressed");
+
+ // Suppress
+ suppressTracing(this._ai.core);
+ Assert.ok(isTracingSuppressed(this._ai.core), "Should be suppressed - check 1");
+ Assert.ok(isTracingSuppressed(this._ai.core), "Should be suppressed - check 2");
+ Assert.ok(isTracingSuppressed(this._ai.core), "Should be suppressed - check 3");
+
+ // Unsuppress
+ unsuppressTracing(this._ai.core);
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Should not be suppressed - check 1");
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Should not be suppressed - check 2");
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Should not be suppressed - check 3");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: span attributes should be preserved when tracing is suppressed",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+
+ // Act - create span with attributes
+ const span = this._ai.startSpan("suppressed-with-attrs", {
+ attributes: {
+ "initial.attr1": "value1",
+ "initial.attr2": 100
+ }
+ });
+
+ Assert.ok(!span!.isRecording(), "Span reports isRecording()=false");
+
+ // Add more attributes
+ span!.setAttribute("runtime.attr", "added-later");
+
+ // Assert - attributes should still be accessible
+ const attributes = (span as any).attributes || {};
+ Assert.ok(attributes["initial.attr1"] === undefined || attributes["runtime.attr"] === undefined,
+ "Attributes should not be stored as span was not recording");
+
+ span!.end();
+ Assert.equal(this._trackCalls.length, 0, "No telemetry should be generated");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: span context should be valid when tracing is suppressed",
+ test: () => {
+ // Arrange
+ suppressTracing(this._ai.core);
+
+ // Act
+ const span = this._ai.startSpan("suppressed-context-test");
+ Assert.ok(!span!.isRecording(), "Span reports isRecording()=false");
+
+ // Assert - span context should be valid
+ const spanContext = span!.spanContext();
+ Assert.ok(spanContext, "Span context should exist");
+ Assert.ok(spanContext.traceId, "Trace ID should exist");
+ Assert.ok(spanContext.spanId, "Span ID should exist");
+ Assert.equal(spanContext.traceId.length, 32, "Trace ID should be 32 hex characters");
+ Assert.equal(spanContext.spanId.length, 16, "Span ID should be 16 hex characters");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: rapid suppression toggling should work correctly",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Act - rapidly toggle suppression
+ for (let i = 0; i < 5; i++) {
+ suppressTracing(this._ai.core);
+ Assert.ok(isTracingSuppressed(this._ai.core), `Should be suppressed on iteration ${i}`);
+
+ unsuppressTracing(this._ai.core);
+ Assert.ok(!isTracingSuppressed(this._ai.core), `Should not be suppressed on iteration ${i}`);
+ }
+
+ // Final state check
+ Assert.ok(!isTracingSuppressed(this._ai.core), "Should end in unsuppressed state");
+
+ // Create a recording span
+ const span = this._ai.startSpan("final-span");
+ Assert.ok(span!.isRecording(), "Span should be recording after toggles");
+ span!.end();
+
+ Assert.equal(this._trackCalls.length, 1, "Telemetry should be tracked");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppression should work with explicit parent context",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Create a recording parent span first
+ const parentSpan = this._ai.startSpan("parent-recording");
+ Assert.ok(parentSpan!.isRecording(), "Parent should be recording");
+ const parentContext = this._ai.getTraceCtx();
+
+ // Suppress tracing
+ suppressTracing(this._ai.core);
+
+ // Act - create child with explicit parent while suppressed
+ const childSpan = this._ai.startSpan("child-suppressed", {
+ kind: eOTelSpanKind.INTERNAL
+ }, parentContext);
+
+ // Assert
+ Assert.ok(!childSpan!.isRecording(), "Child span reports isRecording()=false when suppressed");
+
+ const childContext = childSpan!.spanContext();
+ Assert.equal(childContext.traceId, parentContext!.traceId, "Child should have same traceId as parent");
+
+ unsuppressTracing(this._ai.core);
+
+ childSpan!.end();
+ parentSpan!.end();
+
+ // Only parent should generate telemetry
+ Assert.equal(this._trackCalls.length, 1, "Only parent span should generate telemetry");
+ Assert.equal(this._trackCalls[0].baseData?.name, "parent-recording", "Parent span telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppression should work even with parent context",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Create a recording parent span first
+ const parentSpan = this._ai.startSpan("parent-recording");
+ Assert.ok(parentSpan!.isRecording(), "Parent should be recording");
+ const parentContext = this._ai.getTraceCtx();
+
+ // Suppress tracing
+ suppressTracing(this._ai.core);
+
+ // Act - create child with explicit parent while suppressed
+ const childSpan = this._ai.startSpan("child-suppressed", {
+ kind: eOTelSpanKind.INTERNAL
+ }, parentContext);
+
+ // Assert
+ Assert.ok(!childSpan!.isRecording(), "Child span reports isRecording()=false when suppressed");
+
+ const childContext = childSpan!.spanContext();
+ Assert.equal(childContext.traceId, parentContext!.traceId, "Child should have same traceId as parent");
+
+ childSpan!.end();
+ parentSpan!.end();
+
+ // Only parent should generate telemetry
+ Assert.equal(this._trackCalls.length, 0, "Parent span should not generate telemetry either as suppression is active");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: isTracingSuppressed should handle null/undefined gracefully",
+ test: () => {
+ // Act & Assert - should not throw
+ let result1: boolean;
+ let result2: boolean;
+
+ try {
+ result1 = isTracingSuppressed(null as any);
+ result2 = isTracingSuppressed(undefined as any);
+ Assert.ok(true, "isTracingSuppressed should handle null/undefined without throwing");
+ Assert.ok(!result1, "Should return false for null");
+ Assert.ok(!result2, "Should return false for undefined");
+ } catch (e) {
+ Assert.ok(false, "isTracingSuppressed should not throw for null/undefined");
+ }
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppressTracing with startSpan integration test",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+
+ // Test 1: Normal recording
+ const span1 = this._ai.startSpan("normal-1");
+ Assert.ok(span1!.isRecording(), "Span 1 should be recording");
+ span1!.end();
+ Assert.equal(this._trackCalls.length, 1, "Should have 1 telemetry item");
+
+ // Test 2: Suppress and verify spans still report isRecording()=true but don't send telemetry
+ suppressTracing(this._ai.core);
+ const span2 = this._ai.startSpan("suppressed-1");
+ const span3 = this._ai.startSpan("suppressed-2");
+ Assert.ok(!span2!.isRecording(), "Span 2 reports isRecording()=false when suppressed");
+ Assert.ok(!span3!.isRecording(), "Span 3 reports isRecording()=false when suppressed");
+ span2!.end();
+ span3!.end();
+ Assert.equal(this._trackCalls.length, 1, "Should still have only 1 telemetry item");
+
+ // Test 3: Unsuppress and verify startSpan creates recording spans again
+ unsuppressTracing(this._ai.core);
+ const span4 = this._ai.startSpan("normal-2");
+ Assert.ok(span4!.isRecording(), "Span 4 should be recording");
+ span4!.end();
+ Assert.equal(this._trackCalls.length, 2, "Should have 2 telemetry items");
+
+ // Verify telemetry content
+ Assert.equal(this._trackCalls[0].baseData?.name, "normal-1", "First telemetry is from span1");
+ Assert.equal(this._trackCalls[1].baseData?.name, "normal-2", "Second telemetry is from span4");
+ }
+ });
+
+ this.testCase({
+ name: "TraceSuppression: suppression with nested spans",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ suppressTracing(this._ai.core);
+
+ // Create and set active span manually
+ const span1 = this._ai.startSpan("outer-span");
+ Assert.ok(!span1!.isRecording(), "Outer span reports isRecording()=false");
+
+ // Simulate nested operation
+ const span2 = this._ai.startSpan("inner-span");
+ Assert.ok(!span2!.isRecording(), "Inner span reports isRecording()=false");
+ span2!.end();
+ span1!.end();
+
+ Assert.equal(this._trackCalls.length, 0, "No telemetry for suppressed nested spans");
+ }
+ });
+ }
+}
diff --git a/AISKU/Tests/Unit/src/UseSpan.Tests.ts b/AISKU/Tests/Unit/src/UseSpan.Tests.ts
new file mode 100644
index 000000000..207c35a61
--- /dev/null
+++ b/AISKU/Tests/Unit/src/UseSpan.Tests.ts
@@ -0,0 +1,1164 @@
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/applicationinsights-web";
+import {
+ IAppInsightsCore, IReadableSpan, eOTelSpanKind, eOTelSpanStatusCode, useSpan, ITelemetryItem, ISpanScope, ITraceHost
+} from "@microsoft/applicationinsights-core-js";
+
+export class UseSpanTests extends AITestClass {
+ private static readonly _instrumentationKey = "b7170927-2d1c-44f1-acec-59f4e1751c11";
+ private static readonly _connectionString = `InstrumentationKey=${UseSpanTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+
+ // Track calls to track for validation
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "UseSpanTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: UseSpanTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ // Initialize the SDK
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error("Failed to initialize UseSpan tests: " + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addTests();
+ }
+
+ private addTests(): void {
+
+ this.testCase({
+ name: "UseSpan: useSpan should be available as exported function",
+ test: () => {
+ // Verify that useSpan is available as an import
+ Assert.ok(typeof useSpan === "function", "useSpan should be available as exported function");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should execute function within span context",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-context-test", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.type": "context-execution"
+ }
+ });
+
+ Assert.ok(testSpan, "Test span should be created");
+ Assert.ok(this._ai.core, "Core should be available");
+
+ let capturedActiveSpan: IReadableSpan | null = null;
+ let capturedHost: ITraceHost | null = null;
+ const testFunction = function(this: ISpanScope) {
+ capturedActiveSpan = this.host.getActiveSpan();
+ capturedHost = this.host;
+ return "context-success";
+ };
+
+ // Act
+ const result = useSpan(this._ai.core!, testSpan!, testFunction);
+
+ // Assert
+ Assert.equal(result, "context-success", "Function should execute and return result");
+ Assert.ok(capturedActiveSpan, "Function should have access to active span");
+ Assert.equal(capturedActiveSpan, testSpan, "Active span should be the provided test span");
+ Assert.equal(capturedHost, this._ai.core, "Active host should be the core instance (passed to useSpan)");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should work with telemetry tracking inside span context",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const testSpan = this._ai.startSpan("useSpan-telemetry-test", {
+ attributes: {
+ "operation.name": "telemetry-tracking"
+ }
+ });
+
+ Assert.ok(testSpan, "Test span should be created");
+
+ const telemetryFunction = () => {
+ // Track some telemetry within the span context
+ this._ai.trackEvent({
+ name: "operation-event",
+ properties: {
+ "event.source": "useSpan-context"
+ }
+ });
+
+ this._ai.trackMetric({
+ name: "operation.duration",
+ average: 123.45
+ });
+
+ return "telemetry-tracked";
+ };
+
+ // Act
+ const result = useSpan(this._ai.core!, testSpan!, telemetryFunction);
+
+ // Assert
+ Assert.equal(result, "telemetry-tracked", "Function should complete successfully");
+
+ // End the span to trigger trace generation
+ testSpan!.end();
+
+ // Verify track was called for the span
+ Assert.equal(this._trackCalls.length, 3, "Should have one track call from span ending");
+ const item = this._trackCalls[2];
+ Assert.ok(item.baseData && item.baseData.properties, "Item should have properties");
+ Assert.equal("useSpan-telemetry-test", item.baseData.name, "Should include span name in properties");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should handle complex function arguments and return values",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-arguments-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ const complexFunction = (
+ _scope: ISpanScope,
+ stringArg: string,
+ numberArg: number,
+ objectArg: { key: string; value: number },
+ arrayArg: string[]
+ ) => {
+ return {
+ processedString: stringArg!.toUpperCase(),
+ doubledNumber: numberArg! * 2,
+ extractedValue: objectArg!.value,
+ joinedArray: arrayArg!.join("-"),
+ timestamp: Date.now()
+ };
+ };
+
+ const inputObject = { key: "test-key", value: 42 };
+ const inputArray = ["item1", "item2", "item3"];
+
+ // Act
+ const result = useSpan(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this._ai.core!,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ testSpan!,
+ complexFunction,
+ undefined,
+ "hello world",
+ 10,
+ inputObject,
+ inputArray
+ );
+
+ // Assert
+ Assert.equal(result.processedString, "HELLO WORLD", "String should be processed correctly");
+ Assert.equal(result.doubledNumber, 20, "Number should be doubled correctly");
+ Assert.equal(result.extractedValue, 42, "Object value should be extracted correctly");
+ Assert.equal(result.joinedArray, "item1-item2-item3", "Array should be joined correctly");
+ Assert.ok(result.timestamp > 0, "Timestamp should be generated");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should handle function with this context binding",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-this-binding-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ class TestService {
+ private _serviceId: string;
+ private _multiplier: number;
+
+ constructor(id: string, multiplier: number) {
+ this._serviceId = id;
+ this._multiplier = multiplier;
+ }
+
+ public processValue(_scope: ISpanScope, input: number): { serviceId: string; result: number; multiplied: number } {
+ return {
+ serviceId: this._serviceId,
+ result: input + 100,
+ multiplied: input * this._multiplier
+ };
+ }
+ }
+
+ const service = new TestService("test-service-123", 3);
+
+ // Act
+ const result = useSpan(
+ this._ai.core!,
+ testSpan!,
+ service.processValue,
+ service,
+ 25
+ );
+
+ // Assert
+ Assert.equal(result.serviceId, "test-service-123", "Service ID should be preserved via this binding");
+ Assert.equal(result.result, 125, "Input should be processed correctly");
+ Assert.equal(result.multiplied, 75, "Multiplication should use instance property");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should maintain span context across async-like operations",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-async-like-test", {
+ attributes: {
+ "operation.type": "async-simulation"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ let spanDuringCallback: IReadableSpan | null = null;
+ let callbackExecuted = false;
+
+ const asyncLikeFunction = (scope: ISpanScope, callback: (data: string) => void) => {
+ // Simulate async work that completes synchronously in test
+ let currentSpan = scope.span;
+
+ // Simulate callback execution (would normally be async)
+ setTimeout(() => {
+ spanDuringCallback = this._ai.core!.getActiveSpan();
+ callback("async-data");
+ callbackExecuted = true;
+ }, 0);
+
+ return currentSpan ? (currentSpan as IReadableSpan).name : "no-span";
+ };
+
+ // Act
+ let callbackData = "";
+ const callback = (data: string) => {
+ callbackData = data;
+ };
+
+ const result = useSpan(this._ai.core!, testSpan!, asyncLikeFunction, undefined, callback);
+
+ // Assert
+ Assert.equal(result, "useSpan-async-like-test", "Function should have access to span name");
+
+ // Note: In a real async scenario, the span context wouldn't automatically
+ // propagate to the setTimeout callback without additional context management
+ // This test validates the synchronous behavior of useSpan
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should handle exceptions and preserve span operations",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-exception-test", {
+ attributes: {
+ "test.expects": "exception"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ const exceptionFunction = () => {
+ // Perform some span operations before throwing
+ const activeSpan = this._ai.core!.getActiveSpan();
+ Assert.ok(activeSpan, "Should have active span before exception");
+
+ activeSpan!.setAttribute("operation.status", "error");
+ activeSpan!.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Operation failed with test exception"
+ });
+
+ throw new Error("Test exception for useSpan handling");
+ };
+
+ // Act & Assert
+ let caughtException: Error | null = null;
+ try {
+ useSpan(this._ai.core!, testSpan!, exceptionFunction);
+ } catch (error) {
+ caughtException = error as Error;
+ }
+
+ Assert.ok(caughtException, "Exception should be thrown and caught");
+ Assert.equal(caughtException!.message, "Test exception for useSpan handling", "Exception message should be preserved");
+
+ // Verify span is still valid and operations were applied
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after exception");
+ const readableSpan = testSpan! as IReadableSpan;
+ Assert.ok(!readableSpan.ended, "Span should not be ended by useSpan after exception");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should work with nested span operations and child spans",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const parentSpan = this._ai.startSpan("parent-operation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "operation.name": "parent-process"
+ }
+ });
+ Assert.ok(parentSpan, "Parent span should be created");
+
+ const nestedOperations = () => {
+ // Verify we have the parent span as active
+ const currentActive = this._ai.core!.getActiveSpan();
+ Assert.equal(currentActive, parentSpan, "Parent span should be active");
+
+ // Create child operations within the parent span context
+ const childSpan1 = this._ai.startSpan("child-operation-1", {
+ attributes: { "child.order": 1 }
+ });
+ childSpan1!.setAttribute("child.status", "completed");
+ childSpan1!.end();
+
+ const childSpan2 = this._ai.startSpan("child-operation-2", {
+ attributes: { "child.order": 2 }
+ });
+ childSpan2!.setAttribute("child.status", "completed");
+ childSpan2!.end();
+
+ return "nested-operations-completed";
+ };
+
+ // Act
+ const result = useSpan(this._ai.core!, parentSpan!, nestedOperations);
+
+ // Assert
+ Assert.equal(result, "nested-operations-completed", "Nested operations should complete successfully");
+
+ // End parent span to generate telemetry
+ parentSpan!.end();
+
+ // Should have 3 telemetry items: parent + 2 children
+ Assert.equal(this._trackCalls.length, 3, "Should have telemetry for parent and child spans");
+
+ // Verify span names in properties
+ const spanNames = this._trackCalls.map(item => item.baseData?.name).filter(n => n);
+ Assert.ok(spanNames.some(name => name === "parent-operation"), "Should have parent span telemetry");
+ Assert.ok(spanNames.some(name => name === "child-operation-1"), "Should have child-1 span telemetry");
+ Assert.ok(spanNames.some(name => name === "child-operation-2"), "Should have child-2 span telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should support different return value types",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-return-types-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ // Test various return types
+ const stringResult = useSpan(this._ai.core!, testSpan!, () => "string-result");
+ const numberResult = useSpan(this._ai.core!, testSpan!, () => 42.5);
+ const booleanResult = useSpan(this._ai.core!, testSpan!, () => true);
+ const arrayResult = useSpan(this._ai.core!, testSpan!, () => [1, 2, 3]);
+ const objectResult = useSpan(this._ai.core!, testSpan!, () => ({ key: "value", nested: { prop: 123 } }));
+ const nullResult = useSpan(this._ai.core!, testSpan!, () => null);
+ const undefinedResult = useSpan(this._ai.core!, testSpan!, () => undefined);
+
+ // Assert
+ Assert.equal(stringResult, "string-result", "String return should work");
+ Assert.equal(numberResult, 42.5, "Number return should work");
+ Assert.equal(booleanResult, true, "Boolean return should work");
+ Assert.equal(arrayResult.length, 3, "Array return should work");
+ Assert.equal(arrayResult[1], 2, "Array elements should be preserved");
+ Assert.equal(objectResult.key, "value", "Object properties should be preserved");
+ Assert.equal(objectResult.nested.prop, 123, "Nested object properties should be preserved");
+ Assert.equal(nullResult, null, "Null return should work");
+ Assert.equal(undefinedResult, undefined, "Undefined return should work");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should handle rapid successive calls efficiently",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-performance-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ const iterations = 100;
+ let totalResult = 0;
+
+ // Simple computation function
+ const computeFunction = (_scope: ISpanScope, input: number) => {
+ return input * 2 + 1;
+ };
+
+ const startTime = Date.now();
+
+ // Act - Multiple rapid useSpan calls
+ for (let i = 0; i < iterations; i++) {
+ const result = useSpan(this._ai.core!, testSpan!, computeFunction, undefined, i);
+ totalResult += result;
+ }
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ // Assert
+ const expectedTotal = Array.from({length: iterations}, (_, i) => i * 2 + 1).reduce((sum, val) => sum + val, 0);
+ Assert.equal(totalResult, expectedTotal, "All computations should be correct");
+
+ // Performance assertion - should complete reasonably quickly
+ Assert.ok(duration < 1000, `Performance test should complete quickly: ${duration}ms for ${iterations} iterations`);
+
+ // Verify span is still valid after many operations
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after multiple useSpan calls");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should integrate with AI telemetry correlation",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const operationSpan = this._ai.startSpan("user-operation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "user.id": "user-123",
+ "operation.type": "data-processing"
+ }
+ });
+ Assert.ok(operationSpan, "Operation span should be created");
+
+ const businessLogicFunction = (_scope: ISpanScope, userId: string, dataType: string) => {
+ // Track multiple telemetry items within span context
+ this._ai.trackEvent({
+ name: "data-processing-started",
+ properties: {
+ "user.id": userId,
+ "data.type": dataType,
+ "processing.stage": "initialization"
+ }
+ });
+
+ // Simulate some processing steps
+ for (let step = 1; step <= 3; step++) {
+ this._ai.trackMetric({
+ name: "processing.step.duration",
+ average: step * 10.5,
+ properties: {
+ "step.number": step.toString()
+ }
+ });
+ }
+
+ this._ai.trackEvent({
+ name: "data-processing-completed",
+ properties: {
+ "user.id": userId,
+ "data.type": dataType,
+ "processing.stage": "completion",
+ "steps.completed": "3"
+ }
+ });
+
+ return {
+ userId: userId,
+ dataType: dataType,
+ stepsCompleted: 3,
+ status: "success"
+ };
+ };
+
+ // Act
+ const result = useSpan(
+ this._ai.core!,
+ operationSpan!,
+ businessLogicFunction,
+ undefined,
+ "user-123",
+ "customer-data"
+ );
+
+ // End the span to generate trace
+ operationSpan!.end();
+
+ // Assert
+ Assert.equal(result.userId, "user-123", "User ID should be processed correctly");
+ Assert.equal(result.dataType, "customer-data", "Data type should be processed correctly");
+ Assert.equal(result.stepsCompleted, 3, "All processing steps should be completed");
+ Assert.equal(result.status, "success", "Operation should complete successfully");
+
+ // Verify span telemetry was generated
+ Assert.equal(this._trackCalls.length, 6, "Should have one track call from span ending");
+ const spanItem = this._trackCalls[5];
+ Assert.ok(spanItem.baseData && spanItem.baseData.properties, "Item should have properties");
+ Assert.equal("user-operation", spanItem.baseData.name, "Should include span name");
+
+ // Verify span attributes are included in properties
+ Assert.equal(spanItem.baseData.properties["user.id"], "user-123", "Span attributes should be included in telemetry");
+ Assert.equal(spanItem.baseData.properties["operation.type"], "data-processing", "All span attributes should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should handle empty or no-op functions gracefully",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("useSpan-noop-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ // Test empty function
+ const emptyFunction = () => {};
+
+ // Test function that just returns without doing anything
+ const noOpFunction = () => {
+ return;
+ };
+
+ // Test function that returns undefined explicitly
+ const undefinedFunction = () => {
+ return undefined;
+ };
+
+ // Act
+ const emptyResult = useSpan(this._ai.core!, testSpan!, emptyFunction);
+ const noOpResult = useSpan(this._ai.core!, testSpan!, noOpFunction);
+ const undefinedResult = useSpan(this._ai.core!, testSpan!, undefinedFunction);
+
+ // Assert
+ Assert.equal(emptyResult, undefined, "Empty function should return undefined");
+ Assert.equal(noOpResult, undefined, "No-op function should return undefined");
+ Assert.equal(undefinedResult, undefined, "Undefined function should return undefined");
+
+ // Verify span is still valid
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after no-op functions");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should use ISpanScope as 'this' when no thisArg provided",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("usespan-test", {
+ attributes: { "test.id": "useSpan-this-test" }
+ });
+
+ let capturedThis: any = null;
+ let capturedScopeParam: any = null;
+
+ // Act - call useSpan without thisArg (function receives scope as parameter)
+ const result = useSpan(this._ai.core, span!, function(this: ISpanScope, scope: ISpanScope, arg1: string) {
+ capturedThis = this;
+ capturedScopeParam = scope;
+
+ // Verify 'this' is ISpanScope
+ Assert.ok(this.host, "'this.core' should exist");
+ Assert.ok(this.span, "'this.span' should exist");
+
+ // Verify scope parameter is also ISpanScope
+ Assert.ok(scope.host, "scope.host should exist");
+ Assert.ok(scope.span, "scope.span should exist");
+
+ return `${arg1}-${scope.span.name}`;
+ }, undefined, "result");
+
+ // Assert
+ Assert.equal(result, "result-usespan-test", "Function should execute and return result");
+ Assert.ok(capturedThis, "'this' should be defined");
+ Assert.ok(capturedThis.host, "'this.host' should exist");
+ Assert.ok(capturedThis.span, "'this.span' should exist");
+ Assert.equal(capturedThis.host, this._ai.core, "'this.host' should be the AI core");
+ Assert.equal(capturedThis.span, span, "'this.span' should be the passed span");
+
+ Assert.ok(capturedScopeParam, "scope parameter should be defined");
+ Assert.equal(capturedScopeParam.host, this._ai.core, "scope.host should be the AI core");
+ Assert.equal(capturedScopeParam.span, span, "scope.span should be the passed span");
+
+ // Both 'this' and scope param should be the same ISpanScope instance
+ Assert.equal(capturedThis, capturedScopeParam, "'this' and scope param should be the same ISpanScope instance");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: should use provided thisArg when specified",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("usespan-thisarg-test");
+
+ class ServiceClass {
+ public serviceId: string = "service-456";
+ public getData(prefix: string): string {
+ return `${prefix}-${this.serviceId}`;
+ }
+ }
+
+ const service = new ServiceClass();
+ let capturedThis: any = null;
+ let capturedScopeParam: any = null;
+
+ // Act - call useSpan with explicit thisArg
+ const result = useSpan(this._ai.core, span!, function(this: ServiceClass, scope: ISpanScope) {
+ capturedThis = this;
+ capturedScopeParam = scope;
+
+ // 'this' should be the service instance
+ Assert.equal(this.serviceId, "service-456", "'this.serviceId' should match");
+ Assert.ok(typeof this.getData === "function", "'this.getData' should be a function");
+
+ // scope parameter should still be ISpanScope
+ Assert.ok(scope.host, "scope.host should exist");
+ Assert.ok(scope.span, "scope.span should exist");
+
+ return this.getData("custom");
+ }, service);
+
+ // Assert
+ Assert.equal(result, "custom-service-456", "Function should execute with custom this context");
+ Assert.ok(capturedThis, "'this' should be defined");
+ Assert.equal(capturedThis, service, "'this' should be the service instance");
+ Assert.equal(capturedThis.serviceId, "service-456", "'this.serviceId' should match");
+ Assert.ok(!capturedThis.host, "Custom this should not have host property");
+ Assert.ok(!capturedThis.span, "Custom this should not have span property");
+
+ Assert.ok(capturedScopeParam, "scope parameter should be defined");
+ Assert.ok(capturedScopeParam.host, "scope.host should exist even with custom this");
+ Assert.ok(capturedScopeParam.span, "scope.span should exist even with custom this");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: scope parameter should provide access to core and span operations",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const span = this._ai.startSpan("scope-operations-test");
+
+ // Act - use scope parameter to perform operations
+ useSpan(this._ai.core, span!, (scope: ISpanScope) => {
+ // Use scope.span to set attributes
+ scope.span.setAttribute("operation.name", "data-processing");
+ scope.span.setAttribute("operation.step", 1);
+
+ // Use scope.span to set status
+ scope.span.setStatus({
+ code: 0,
+ message: "Operation successful"
+ });
+
+ // Use scope.span to get context
+ const spanContext = scope.span.spanContext();
+ Assert.ok(spanContext.traceId, "Should access span context via scope");
+ Assert.ok(spanContext.spanId, "Should access span ID via scope");
+
+ // Verify span name
+ Assert.equal(scope.span.name, "scope-operations-test", "Span name should be accessible");
+ });
+
+ // Assert
+ Assert.ok(span, "Span should exist");
+ Assert.equal(span!.name, "scope-operations-test", "Span name should match");
+
+ span!.end();
+
+ Assert.equal(this._trackCalls.length, 1, "Should generate telemetry");
+ Assert.ok(this._trackCalls[0].baseData?.properties, "Should have properties");
+ Assert.equal(this._trackCalls[0].baseData.properties["operation.name"], "data-processing", "Attributes should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: 'this' binding with nested useSpan calls",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("nested-calls-test");
+
+ const outerContext = {
+ contextName: "outer",
+ value: 100
+ };
+
+ let outerThisCapture: any = null;
+ let innerThisCapture: any = null;
+ let ai = this._ai;
+
+ // Act - nested useSpan calls with different thisArg
+ useSpan(this._ai.core, span!, function(this: typeof outerContext, outerScope: ISpanScope) {
+ outerThisCapture = this;
+ Assert.equal(this.contextName, "outer", "Outer 'this' should be outer context");
+ Assert.equal(this.value, 100, "Outer 'this.value' should match");
+
+ const innerSpan = ai.startSpan("inner-nested-span");
+
+ useSpan(ai.core, innerSpan!, function(this: ISpanScope, innerScope: ISpanScope) {
+ innerThisCapture = this;
+ // Inner call without explicit thisArg - should be ISpanScope
+ Assert.ok(this.host, "Inner 'this' should be ISpanScope");
+ Assert.ok(this.span, "Inner 'this.span' should exist");
+ Assert.equal(this.span.name, "inner-nested-span", "Inner span name should match");
+ });
+
+ innerSpan!.end();
+ }, outerContext);
+
+ // Assert
+ Assert.ok(outerThisCapture, "Outer 'this' should be captured");
+ Assert.equal(outerThisCapture.contextName, "outer", "Outer context should be preserved");
+
+ Assert.ok(innerThisCapture, "Inner 'this' should be captured");
+ Assert.ok(innerThisCapture.host, "Inner 'this' should have host");
+ Assert.ok(innerThisCapture.span, "Inner 'this' should have span");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: verify scope.restore() is called to restore previous active span",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-span");
+ const innerSpan = this._ai.startSpan("inner-span");
+
+ let activeSpanBeforeUseSpan: any = null;
+ let activeSpanInsideUseSpan: any = null;
+ let activeSpanAfterUseSpan: any = null;
+
+ // Act
+ activeSpanBeforeUseSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+
+ useSpan(this._ai.core, innerSpan!, (scope: ISpanScope) => {
+ activeSpanInsideUseSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+ Assert.equal(activeSpanInsideUseSpan, innerSpan, "Active span inside useSpan should be inner span");
+ });
+
+ activeSpanAfterUseSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+
+ // Assert
+ // Active span should be restored after useSpan completes
+ Assert.equal(activeSpanAfterUseSpan, activeSpanBeforeUseSpan,
+ "Active span should be restored after useSpan completes");
+
+ innerSpan!.end();
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: trace context should match active span context inside useSpan",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("trace-context-match-test", {
+ attributes: {
+ "test.type": "trace-context-validation"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ let traceCtxInsideUseSpan: any = null;
+ let spanContextInsideUseSpan: any = null;
+ let activeSpanInsideUseSpan: any = null;
+
+ // Act
+ useSpan(this._ai.core, testSpan!, (scope: ISpanScope) => {
+ // Get trace context from core
+ traceCtxInsideUseSpan = this._ai.core.getTraceCtx(false);
+
+ // Get span context from the span
+ spanContextInsideUseSpan = scope.span.spanContext();
+
+ // Get active span
+ activeSpanInsideUseSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+ });
+
+ // Assert
+ Assert.ok(traceCtxInsideUseSpan, "Trace context should exist inside useSpan");
+ Assert.ok(spanContextInsideUseSpan, "Span context should exist");
+ Assert.ok(activeSpanInsideUseSpan, "Active span should be set");
+
+ // Verify active span matches the useSpan span
+ Assert.equal(activeSpanInsideUseSpan, testSpan, "Active span should be the useSpan span");
+
+ // Verify trace context matches span context
+ Assert.equal(traceCtxInsideUseSpan.traceId, spanContextInsideUseSpan.traceId,
+ "Trace context traceId should match span context traceId");
+ Assert.equal(traceCtxInsideUseSpan.spanId, spanContextInsideUseSpan.spanId,
+ "Trace context spanId should match span context spanId");
+ Assert.equal(traceCtxInsideUseSpan.traceFlags, spanContextInsideUseSpan.traceFlags,
+ "Trace context traceFlags should match span context traceFlags");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: trace context updates when switching between nested useSpan calls",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-trace-span");
+ const innerSpan = this._ai.startSpan("inner-trace-span");
+
+ let outerTraceCtx: any = null;
+ let outerSpanCtx: any = null;
+ let innerTraceCtx: any = null;
+ let innerSpanCtx: any = null;
+
+ // Act
+ useSpan(this._ai.core, outerSpan!, (outerScope: ISpanScope) => {
+ outerTraceCtx = this._ai.core.getTraceCtx(false);
+ outerSpanCtx = outerScope.span.spanContext();
+
+ // Verify outer trace context matches outer span
+ Assert.equal(outerTraceCtx.spanId, outerSpanCtx.spanId,
+ "Outer trace context should match outer span");
+
+ // Nested useSpan with different span
+ useSpan(this._ai.core, innerSpan!, (innerScope: ISpanScope) => {
+ innerTraceCtx = this._ai.core.getTraceCtx(false);
+ innerSpanCtx = innerScope.span.spanContext();
+
+ // Verify inner trace context matches inner span
+ Assert.equal(innerTraceCtx.spanId, innerSpanCtx.spanId,
+ "Inner trace context should match inner span");
+
+ // Verify inner context is different from outer
+ Assert.notEqual(innerTraceCtx.spanId, outerTraceCtx.spanId,
+ "Inner and outer trace contexts should have different spanIds");
+ });
+
+ // After inner useSpan, verify we're back to outer context
+ const restoredTraceCtx = this._ai.core.getTraceCtx(false);
+ Assert.equal(restoredTraceCtx.spanId, outerSpanCtx.spanId,
+ "Trace context should be restored to outer span after inner useSpan completes");
+ });
+
+ outerSpan!.end();
+ innerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: child spans created inside useSpan inherit correct parent context",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-for-child-test");
+
+ let childSpanContext: any = null;
+ let parentSpanContext: any = null;
+
+ // Act
+ useSpan(this._ai.core, parentSpan!, (scope: ISpanScope) => {
+ parentSpanContext = scope.span.spanContext();
+
+ // Create a child span while parent is active
+ const childSpan = this._ai.startSpan("child-span-in-useSpan");
+ childSpanContext = childSpan!.spanContext();
+
+ // Verify trace context matches parent
+ const traceCtx = this._ai.core.getTraceCtx(false);
+ Assert.equal(traceCtx.spanId, parentSpanContext.spanId,
+ "Trace context should match parent span inside useSpan");
+
+ childSpan!.end();
+ });
+
+ // Assert
+ Assert.ok(childSpanContext, "Child span context should exist");
+ Assert.ok(parentSpanContext, "Parent span context should exist");
+
+ // Child should have same traceId as parent but different spanId
+ Assert.equal(childSpanContext.traceId, parentSpanContext.traceId,
+ "Child span should have same traceId as parent");
+ Assert.notEqual(childSpanContext.spanId, parentSpanContext.spanId,
+ "Child span should have different spanId from parent");
+
+ parentSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: trace context is restored after useSpan completes",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("temporary-trace-span");
+
+ let traceCtxBefore: any = null;
+ let traceCtxInside: any = null;
+ let traceCtxAfter: any = null;
+
+ // Act
+ traceCtxBefore = this._ai.core.getTraceCtx(false);
+
+ useSpan(this._ai.core, testSpan!, () => {
+ traceCtxInside = this._ai.core.getTraceCtx(false);
+ });
+
+ traceCtxAfter = this._ai.core.getTraceCtx(false);
+
+ // Assert
+ Assert.ok(traceCtxBefore, "Trace context should exist before useSpan (created by startSpan)");
+ Assert.ok(traceCtxInside, "Trace context should exist inside useSpan");
+ Assert.equal(traceCtxInside.spanId, testSpan!.spanContext().spanId,
+ "Trace context inside useSpan should match the test span");
+ Assert.ok(traceCtxAfter, "Trace context should exist after useSpan");
+ Assert.equal(traceCtxAfter.spanId, traceCtxBefore.spanId,
+ "Trace context should be restored to previous state after useSpan");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: trace context reflects parent span when useSpan is nested in another active span",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-active-span");
+ const provider = this._ai.core.getTraceProvider();
+
+ this._ai.setActiveSpan(outerSpan!);
+
+ const innerSpan = this._ai.startSpan("inner-usespan-span");
+
+ let outerSpanCtx: any = null;
+ let traceCtxBeforeUseSpan: any = null;
+ let traceCtxInsideUseSpan: any = null;
+ let traceCtxAfterUseSpan: any = null;
+
+ // Act
+ outerSpanCtx = outerSpan!.spanContext();
+ traceCtxBeforeUseSpan = this._ai.core.getTraceCtx(false);
+
+ // Verify initial trace context matches outer span
+ Assert.equal(traceCtxBeforeUseSpan.spanId, outerSpanCtx.spanId,
+ "Trace context should initially match outer span");
+
+ useSpan(this._ai.core, innerSpan!, (scope: ISpanScope) => {
+ traceCtxInsideUseSpan = this._ai.core.getTraceCtx(false);
+ const innerSpanCtx = scope.span.spanContext();
+
+ // Inside useSpan, trace context should match inner span
+ Assert.equal(traceCtxInsideUseSpan.spanId, innerSpanCtx.spanId,
+ "Trace context inside useSpan should match inner span");
+ });
+
+ traceCtxAfterUseSpan = this._ai.core.getTraceCtx(false);
+
+ // After useSpan, trace context should be restored to outer span
+ Assert.equal(traceCtxAfterUseSpan.spanId, outerSpanCtx.spanId,
+ "Trace context should be restored to outer span after useSpan");
+
+ innerSpan!.end();
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: trace context traceState is accessible inside useSpan",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("tracestate-test-span");
+
+ let traceStateInside: any = null;
+
+ // Act
+ useSpan(this._ai.core, testSpan!, () => {
+ const traceCtx = this._ai.core.getTraceCtx(false);
+ traceStateInside = traceCtx ? traceCtx.traceState : null;
+ });
+
+ // Assert
+ Assert.ok(traceStateInside !== undefined,
+ "Trace state should be accessible inside useSpan");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: span created inside useSpan has parent context matching outer trace context",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-parent-span");
+
+ this._ai.setActiveSpan(outerSpan!);
+
+ let outerTraceCtx: any = null;
+ let innerSpanParentCtx: any = null;
+ let innerSpanCreated: any = null;
+
+ // Act
+ outerTraceCtx = this._ai.core.getTraceCtx(false);
+
+ useSpan(this._ai.core, outerSpan!, (scope: ISpanScope) => {
+ // Create a new span inside useSpan
+ innerSpanCreated = this._ai.startSpan("inner-child-span");
+
+ // Get the parent context of the newly created span
+ if (innerSpanCreated) {
+ innerSpanParentCtx = innerSpanCreated.parentSpanContext;
+ }
+
+ innerSpanCreated!.end();
+ });
+
+ // Assert
+ Assert.ok(outerTraceCtx, "Outer trace context should exist");
+ Assert.ok(innerSpanParentCtx, "Inner span should have parent context");
+
+ // Verify parent context matches outer trace context
+ Assert.equal(innerSpanParentCtx.traceId, outerTraceCtx.traceId,
+ "Inner span parent traceId should match outer trace context traceId");
+ Assert.equal(innerSpanParentCtx.spanId, outerTraceCtx.spanId,
+ "Inner span parent spanId should match outer trace context spanId");
+
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: span parent context matches trace context when useSpan wraps different span",
+ test: () => {
+ // Arrange - Create initial trace context
+ const contextSpan = this._ai.startSpan("context-span");
+
+ this._ai.setActiveSpan(contextSpan!);
+
+ const contextTraceCtx = this._ai.core.getTraceCtx(false);
+
+ // Create a different span to use in useSpan
+ const wrapperSpan = this._ai.startSpan("wrapper-span");
+
+ let spanCreatedInCallback: any = null;
+ let spanParentCtx: any = null;
+
+ // Act - useSpan with a different span than what's in trace context
+ useSpan(this._ai.core, wrapperSpan!, (scope: ISpanScope) => {
+ // The active span is now wrapperSpan
+ // Create a child span - it should have wrapperSpan as parent
+ spanCreatedInCallback = this._ai.startSpan("child-of-wrapper");
+
+ if (spanCreatedInCallback) {
+ spanParentCtx = spanCreatedInCallback.parentSpanContext;
+ }
+
+ spanCreatedInCallback!.end();
+ });
+
+ // Assert
+ Assert.ok(spanParentCtx, "Child span should have parent context");
+
+ // Parent should be wrapperSpan (the useSpan span), not contextSpan
+ const wrapperSpanCtx = wrapperSpan!.spanContext();
+ Assert.equal(spanParentCtx.spanId, wrapperSpanCtx.spanId,
+ "Child span parent should be the wrapper span from useSpan");
+ Assert.notEqual(spanParentCtx.spanId, contextTraceCtx.spanId,
+ "Child span parent should NOT be the original context span");
+
+ wrapperSpan!.end();
+ contextSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "UseSpan: multiple nested spans maintain correct parent-child relationships with trace context",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("root-span");
+
+ this._ai.setActiveSpan(rootSpan!);
+
+ const rootTraceCtx = this._ai.core.getTraceCtx(false);
+ const level1Span = this._ai.startSpan("level1-span");
+
+ let level2SpanParent: any = null;
+ let level2SpanCtx: any = null;
+ let level3SpanParent: any = null;
+
+ // Act - Nested useSpan calls
+ useSpan(this._ai.core, level1Span!, (scope1: ISpanScope) => {
+ const level1TraceCtx = this._ai.core.getTraceCtx(false);
+
+ // Create level2 span - should have level1 as parent
+ const level2Span = this._ai.startSpan("level2-span");
+ if (level2Span) {
+ level2SpanParent = level2Span.parentSpanContext;
+ level2SpanCtx = level2Span.spanContext();
+ }
+
+ useSpan(this._ai.core, level2Span!, (scope2: ISpanScope) => {
+ const level2TraceCtx = this._ai.core.getTraceCtx(false);
+
+ // Create level3 span - should have level2 as parent
+ const level3Span = this._ai.startSpan("level3-span");
+ if (level3Span) {
+ level3SpanParent = level3Span.parentSpanContext;
+ }
+
+ // Verify level3 parent matches level2 trace context
+ Assert.equal(level3SpanParent.spanId, level2TraceCtx.spanId,
+ "Level3 span parent should match level2 trace context");
+
+ level3Span!.end();
+ });
+
+ // Verify level2 parent matches level1 trace context
+ Assert.equal(level2SpanParent.spanId, level1TraceCtx.spanId,
+ "Level2 span parent should match level1 trace context");
+
+ level2Span!.end();
+ });
+
+ // Assert
+ Assert.ok(level2SpanParent, "Level2 span should have parent context");
+ Assert.ok(level2SpanCtx, "Level2 span context should exist");
+ Assert.ok(level3SpanParent, "Level3 span should have parent context");
+
+ // Verify the chain: root -> level1 -> level2 -> level3
+ Assert.equal(level2SpanParent.spanId, level1Span!.spanContext().spanId,
+ "Level2 parent should be level1");
+ Assert.equal(level3SpanParent.spanId, level2SpanCtx.spanId,
+ "Level3 parent should be level2");
+
+ level1Span!.end();
+ rootSpan!.end();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/AISKU/Tests/Unit/src/WithSpan.Tests.ts b/AISKU/Tests/Unit/src/WithSpan.Tests.ts
new file mode 100644
index 000000000..534520037
--- /dev/null
+++ b/AISKU/Tests/Unit/src/WithSpan.Tests.ts
@@ -0,0 +1,1114 @@
+import { AITestClass, Assert } from '@microsoft/ai-test-framework';
+import { ApplicationInsights } from '../../../src/applicationinsights-web';
+import {
+ IReadableSpan, eOTelSpanKind, eOTelSpanStatusCode, withSpan, ITelemetryItem, ISpanScope, ITraceHost
+} from "@microsoft/applicationinsights-core-js";
+export class WithSpanTests extends AITestClass {
+ private static readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
+ private static readonly _connectionString = `InstrumentationKey=${WithSpanTests._instrumentationKey}`;
+
+ private _ai!: ApplicationInsights;
+
+ // Track calls to track for validation
+ private _trackCalls: ITelemetryItem[] = [];
+
+ constructor(testName?: string) {
+ super(testName || "WithSpanTests");
+ }
+
+ public testInitialize() {
+ try {
+ this.useFakeServer = false;
+ this._trackCalls = [];
+
+ this._ai = new ApplicationInsights({
+ config: {
+ connectionString: WithSpanTests._connectionString,
+ disableAjaxTracking: false,
+ disableXhr: false,
+ maxBatchInterval: 0,
+ disableExceptionTracking: false
+ }
+ });
+
+ // Initialize the SDK
+ this._ai.loadAppInsights();
+
+ // Hook core.track to capture calls
+ const originalTrack = this._ai.core.track;
+ this._ai.core.track = (item: ITelemetryItem) => {
+ this._trackCalls.push(item);
+ return originalTrack.call(this._ai.core, item);
+ };
+
+ } catch (e) {
+ console.error('Failed to initialize WithSpan tests: ' + e);
+ throw e;
+ }
+ }
+
+ public testFinishedCleanup() {
+ if (this._ai && this._ai.unload) {
+ this._ai.unload(false);
+ }
+ }
+
+ public registerTests() {
+ this.addTests();
+ }
+
+ private addTests(): void {
+
+ this.testCase({
+ name: "WithSpan: withSpan should be available as exported function",
+ test: () => {
+ // Verify that withSpan is available as an import
+ Assert.ok(typeof withSpan === 'function', "withSpan should be available as exported function");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should execute function within span context",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-context-test", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "test.type": "context-execution"
+ }
+ });
+
+ Assert.ok(testSpan, "Test span should be created");
+ Assert.ok(this._ai.core, "Core should be available");
+
+ let capturedActiveSpan: IReadableSpan | null = null;
+ const testFunction = () => {
+ capturedActiveSpan = this._ai.core!.getActiveSpan();
+ return "context-success";
+ };
+
+ // Act
+ const result = withSpan(this._ai.core!, testSpan!, testFunction);
+
+ // Assert
+ Assert.equal(result, "context-success", "Function should execute and return result");
+ Assert.ok(capturedActiveSpan, "Function should have access to active span");
+ Assert.equal(capturedActiveSpan, testSpan, "Active span should be the provided test span");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should work with telemetry tracking inside span context",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const testSpan = this._ai.startSpan("withSpan-telemetry-test", {
+ attributes: {
+ "operation.name": "telemetry-tracking"
+ }
+ });
+
+ Assert.ok(testSpan, "Test span should be created");
+
+ const telemetryFunction = () => {
+ // Track some telemetry within the span context
+ this._ai.trackEvent({
+ name: "operation-event",
+ properties: {
+ "event.source": "withSpan-context"
+ }
+ });
+
+ this._ai.trackMetric({
+ name: "operation.duration",
+ average: 123.45
+ });
+
+ return "telemetry-tracked";
+ };
+
+ // Act
+ const result = withSpan(this._ai.core!, testSpan!, telemetryFunction);
+
+ // Assert
+ Assert.equal(result, "telemetry-tracked", "Function should complete successfully");
+
+ // End the span to trigger trace generation
+ testSpan!.end();
+
+ // Verify track was called for the span
+ Assert.equal(this._trackCalls.length, 3, "Should have one track call from span ending");
+ const item = this._trackCalls[2];
+ Assert.ok(item.baseData && item.baseData.properties, "Item should have properties");
+ Assert.equal("withSpan-telemetry-test", item.baseData.name, "Should include span name in properties");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should handle complex function arguments and return values",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-arguments-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ const complexFunction = (
+ stringArg: string,
+ numberArg: number,
+ objectArg: { key: string; value: number },
+ arrayArg: string[]
+ ) => {
+ return {
+ processedString: stringArg.toUpperCase(),
+ doubledNumber: numberArg * 2,
+ extractedValue: objectArg.value,
+ joinedArray: arrayArg.join('-'),
+ timestamp: Date.now()
+ };
+ };
+
+ const inputObject = { key: "test-key", value: 42 };
+ const inputArray = ["item1", "item2", "item3"];
+
+ // Act
+ const result = withSpan(
+ this._ai.core!,
+ testSpan!,
+ complexFunction,
+ undefined,
+ "hello world",
+ 10,
+ inputObject,
+ inputArray
+ );
+
+ // Assert
+ Assert.equal(result.processedString, "HELLO WORLD", "String should be processed correctly");
+ Assert.equal(result.doubledNumber, 20, "Number should be doubled correctly");
+ Assert.equal(result.extractedValue, 42, "Object value should be extracted correctly");
+ Assert.equal(result.joinedArray, "item1-item2-item3", "Array should be joined correctly");
+ Assert.ok(result.timestamp > 0, "Timestamp should be generated");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should handle function with this context binding",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-this-binding-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ class TestService {
+ private _serviceId: string;
+ private _multiplier: number;
+
+ constructor(id: string, multiplier: number) {
+ this._serviceId = id;
+ this._multiplier = multiplier;
+ }
+
+ public processValue(input: number): { serviceId: string; result: number; multiplied: number } {
+ return {
+ serviceId: this._serviceId,
+ result: input + 100,
+ multiplied: input * this._multiplier
+ };
+ }
+ }
+
+ const service = new TestService("test-service-123", 3);
+
+ // Act
+ const result = withSpan(
+ this._ai.core!,
+ testSpan!,
+ service.processValue,
+ service,
+ 25
+ );
+
+ // Assert
+ Assert.equal(result.serviceId, "test-service-123", "Service ID should be preserved via this binding");
+ Assert.equal(result.result, 125, "Input should be processed correctly");
+ Assert.equal(result.multiplied, 75, "Multiplication should use instance property");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should maintain span context across async-like operations",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-async-like-test", {
+ attributes: {
+ "operation.type": "async-simulation"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ let spanDuringCallback: IReadableSpan | null = null;
+ let callbackExecuted = false;
+
+ const asyncLikeFunction = (callback: (data: string) => void) => {
+ // Simulate async work that completes synchronously in test
+ const currentSpan = this._ai.core!.getActiveSpan();
+
+ // Simulate callback execution (would normally be async)
+ setTimeout(() => {
+ spanDuringCallback = this._ai.core!.getActiveSpan();
+ callback("async-data");
+ callbackExecuted = true;
+ }, 0);
+
+ return currentSpan ? (currentSpan as IReadableSpan).name : "no-span";
+ };
+
+ // Act
+ let callbackData = "";
+ const callback = (data: string) => {
+ callbackData = data;
+ };
+
+ const result = withSpan(this._ai.core!, testSpan!, asyncLikeFunction, undefined, callback);
+
+ // Assert
+ Assert.equal(result, "withSpan-async-like-test", "Function should have access to span name");
+
+ // Note: In a real async scenario, the span context wouldn't automatically
+ // propagate to the setTimeout callback without additional context management
+ // This test validates the synchronous behavior of withSpan
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should handle exceptions and preserve span operations",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-exception-test", {
+ attributes: {
+ "test.expects": "exception"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ const exceptionFunction = () => {
+ // Perform some span operations before throwing
+ const activeSpan = this._ai.core!.getActiveSpan();
+ Assert.ok(activeSpan, "Should have active span before exception");
+
+ activeSpan!.setAttribute("operation.status", "error");
+ activeSpan!.setStatus({
+ code: eOTelSpanStatusCode.ERROR,
+ message: "Operation failed with test exception"
+ });
+
+ throw new Error("Test exception for withSpan handling");
+ };
+
+ // Act & Assert
+ let caughtException: Error | null = null;
+ try {
+ withSpan(this._ai.core!, testSpan!, exceptionFunction);
+ } catch (error) {
+ caughtException = error as Error;
+ }
+
+ Assert.ok(caughtException, "Exception should be thrown and caught");
+ Assert.equal(caughtException!.message, "Test exception for withSpan handling", "Exception message should be preserved");
+
+ // Verify span is still valid and operations were applied
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after exception");
+ const readableSpan = testSpan! as IReadableSpan;
+ Assert.ok(!readableSpan.ended, "Span should not be ended by withSpan after exception");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should work with nested span operations and child spans",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const parentSpan = this._ai.startSpan("parent-operation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "operation.name": "parent-process"
+ }
+ });
+ Assert.ok(parentSpan, "Parent span should be created");
+
+ const nestedOperations = () => {
+ // Verify we have the parent span as active
+ const currentActive = this._ai.core!.getActiveSpan();
+ Assert.equal(currentActive, parentSpan, "Parent span should be active");
+
+ // Create child operations within the parent span context
+ const childSpan1 = this._ai.startSpan("child-operation-1", {
+ attributes: { "child.order": 1 }
+ });
+ childSpan1!.setAttribute("child.status", "completed");
+ childSpan1!.end();
+
+ const childSpan2 = this._ai.startSpan("child-operation-2", {
+ attributes: { "child.order": 2 }
+ });
+ childSpan2!.setAttribute("child.status", "completed");
+ childSpan2!.end();
+
+ return "nested-operations-completed";
+ };
+
+ // Act
+ const result = withSpan(this._ai.core!, parentSpan!, nestedOperations);
+
+ // Assert
+ Assert.equal(result, "nested-operations-completed", "Nested operations should complete successfully");
+
+ // End parent span to generate telemetry
+ parentSpan!.end();
+
+ // Should have 3 telemetry items: parent + 2 children
+ Assert.equal(this._trackCalls.length, 3, "Should have telemetry for parent and child spans");
+
+ // Verify span names in properties
+ const spanNames = this._trackCalls.map(item => item.baseData?.name).filter(n => n);
+ Assert.ok(spanNames.some(name => name === "parent-operation"), "Should have parent span telemetry");
+ Assert.ok(spanNames.some(name => name === "child-operation-1"), "Should have child-1 span telemetry");
+ Assert.ok(spanNames.some(name => name === "child-operation-2"), "Should have child-2 span telemetry");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should support different return value types",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-return-types-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ // Test various return types
+ const stringResult = withSpan(this._ai.core!, testSpan!, () => "string-result");
+ const numberResult = withSpan(this._ai.core!, testSpan!, () => 42.5);
+ const booleanResult = withSpan(this._ai.core!, testSpan!, () => true);
+ const arrayResult = withSpan(this._ai.core!, testSpan!, () => [1, 2, 3]);
+ const objectResult = withSpan(this._ai.core!, testSpan!, () => ({ key: "value", nested: { prop: 123 } }));
+ const nullResult = withSpan(this._ai.core!, testSpan!, () => null);
+ const undefinedResult = withSpan(this._ai.core!, testSpan!, () => undefined);
+
+ // Assert
+ Assert.equal(stringResult, "string-result", "String return should work");
+ Assert.equal(numberResult, 42.5, "Number return should work");
+ Assert.equal(booleanResult, true, "Boolean return should work");
+ Assert.equal(arrayResult.length, 3, "Array return should work");
+ Assert.equal(arrayResult[1], 2, "Array elements should be preserved");
+ Assert.equal(objectResult.key, "value", "Object properties should be preserved");
+ Assert.equal(objectResult.nested.prop, 123, "Nested object properties should be preserved");
+ Assert.equal(nullResult, null, "Null return should work");
+ Assert.equal(undefinedResult, undefined, "Undefined return should work");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should handle rapid successive calls efficiently",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-performance-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ const iterations = 100;
+ let totalResult = 0;
+
+ // Simple computation function
+ const computeFunction = (input: number) => {
+ return input * 2 + 1;
+ };
+
+ const startTime = Date.now();
+
+ // Act - Multiple rapid withSpan calls
+ for (let i = 0; i < iterations; i++) {
+ const result = withSpan(this._ai.core!, testSpan!, computeFunction, undefined, i);
+ totalResult += result;
+ }
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ // Assert
+ const expectedTotal = Array.from({length: iterations}, (_, i) => i * 2 + 1).reduce((sum, val) => sum + val, 0);
+ Assert.equal(totalResult, expectedTotal, "All computations should be correct");
+
+ // Performance assertion - should complete reasonably quickly
+ Assert.ok(duration < 1000, `Performance test should complete quickly: ${duration}ms for ${iterations} iterations`);
+
+ // Verify span is still valid after many operations
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after multiple withSpan calls");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should integrate with AI telemetry correlation",
+ test: () => {
+ // Arrange
+ this._trackCalls = [];
+ const operationSpan = this._ai.startSpan("user-operation", {
+ kind: eOTelSpanKind.SERVER,
+ attributes: {
+ "user.id": "user-123",
+ "operation.type": "data-processing"
+ }
+ });
+ Assert.ok(operationSpan, "Operation span should be created");
+
+ const businessLogicFunction = (userId: string, dataType: string) => {
+ // Track multiple telemetry items within span context
+ this._ai.trackEvent({
+ name: "data-processing-started",
+ properties: {
+ "user.id": userId,
+ "data.type": dataType,
+ "processing.stage": "initialization"
+ }
+ });
+
+ // Simulate some processing steps
+ for (let step = 1; step <= 3; step++) {
+ this._ai.trackMetric({
+ name: "processing.step.duration",
+ average: step * 10.5,
+ properties: {
+ "step.number": step.toString()
+ }
+ });
+ }
+
+ this._ai.trackEvent({
+ name: "data-processing-completed",
+ properties: {
+ "user.id": userId,
+ "data.type": dataType,
+ "processing.stage": "completion",
+ "steps.completed": "3"
+ }
+ });
+
+ return {
+ userId: userId,
+ dataType: dataType,
+ stepsCompleted: 3,
+ status: "success"
+ };
+ };
+
+ // Act
+ const result = withSpan(
+ this._ai.core!,
+ operationSpan!,
+ businessLogicFunction,
+ undefined,
+ "user-123",
+ "customer-data"
+ );
+
+ // End the span to generate trace
+ operationSpan!.end();
+
+ // Assert
+ Assert.equal(result.userId, "user-123", "User ID should be processed correctly");
+ Assert.equal(result.dataType, "customer-data", "Data type should be processed correctly");
+ Assert.equal(result.stepsCompleted, 3, "All processing steps should be completed");
+ Assert.equal(result.status, "success", "Operation should complete successfully");
+
+ // Verify span telemetry was generated
+ Assert.equal(this._trackCalls.length, 6, "Should have one track call from span ending");
+ const spanItem = this._trackCalls[5];
+ Assert.ok(spanItem.baseData && spanItem.baseData.properties, "Item should have properties");
+ Assert.equal("user-operation", spanItem.baseData.name, "Should include span name");
+
+ // Verify span attributes are included in properties
+ Assert.equal(spanItem.baseData.properties["user.id"], "user-123", "Span attributes should be included in telemetry");
+ Assert.equal(spanItem.baseData.properties["operation.type"], "data-processing", "All span attributes should be preserved");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should handle empty or no-op functions gracefully",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("withSpan-noop-test");
+ Assert.ok(testSpan, "Test span should be created");
+
+ // Test empty function
+ const emptyFunction = () => {};
+
+ // Test function that just returns without doing anything
+ const noOpFunction = () => {
+ return;
+ };
+
+ // Test function that returns undefined explicitly
+ const undefinedFunction = () => {
+ return undefined;
+ };
+
+ // Act
+ const emptyResult = withSpan(this._ai.core!, testSpan!, emptyFunction);
+ const noOpResult = withSpan(this._ai.core!, testSpan!, noOpFunction);
+ const undefinedResult = withSpan(this._ai.core!, testSpan!, undefinedFunction);
+
+ // Assert
+ Assert.equal(emptyResult, undefined, "Empty function should return undefined");
+ Assert.equal(noOpResult, undefined, "No-op function should return undefined");
+ Assert.equal(undefinedResult, undefined, "Undefined function should return undefined");
+
+ // Verify span is still valid
+ Assert.ok(testSpan!.isRecording(), "Span should still be recording after no-op functions");
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should use ISpanScope as 'this' when no thisArg provided",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("test-span", {
+ attributes: { "test.id": "withSpan-this-test" }
+ });
+
+ let capturedThis: any = null;
+ let capturedHost: ITraceHost | null = null;
+ let capturedSpan: any = null;
+
+ // Act - call withSpan without thisArg
+ const result = withSpan(this._ai.core, span!, function(this: ISpanScope, arg1: string, arg2: number) {
+ capturedThis = this;
+ capturedHost = this.host;
+ capturedSpan = this.span;
+ return `${arg1}-${arg2}`;
+ }, undefined, "test", 42);
+
+ // Assert
+ Assert.equal(result, "test-42", "Function should execute and return result");
+ Assert.ok(capturedThis, "'this' should be defined");
+ Assert.ok(capturedThis.host, "'this.host' should exist");
+ Assert.ok(capturedThis.span, "'this.span' should exist");
+ Assert.equal(capturedHost, this._ai.core, "'this.host' should be the AI core");
+ Assert.equal(capturedSpan, span, "'this.span' should be the passed span");
+ Assert.equal(capturedThis.span.name, "test-span", "'this.span.name' should match");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: should use provided thisArg when specified",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("test-span-thisarg");
+
+ const customContext = {
+ contextId: "custom-123",
+ multiplier: 10
+ };
+
+ let capturedThis: any = null;
+
+ // Act - call withSpan with explicit thisArg
+ const result = withSpan(this._ai.core, span!, function(this: typeof customContext, arg1: number) {
+ capturedThis = this;
+ return arg1 * this.multiplier;
+ }, customContext, 5);
+
+ // Assert
+ Assert.equal(result, 50, "Function should execute with custom this context");
+ Assert.ok(capturedThis, "'this' should be defined");
+ Assert.equal(capturedThis, customContext, "'this' should be the custom context");
+ Assert.equal(capturedThis.contextId, "custom-123", "'this.contextId' should match");
+ Assert.equal(capturedThis.multiplier, 10, "'this.multiplier' should match");
+ Assert.ok(!capturedThis.core, "Custom this should not have core property");
+ Assert.ok(!capturedThis.span, "Custom this should not have span property");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: arrow functions should not override 'this' binding",
+ test: () => {
+ // Arrange
+ const span = this._ai.startSpan("arrow-function-test");
+
+ // Act - arrow functions capture their lexical 'this'
+ const result = withSpan(this._ai.core, span!, (arg: string) => {
+ // Arrow function - 'this' is lexically bound to the test class instance
+ Assert.ok(this._ai, "Arrow function should have access to test class 'this'");
+ return `arrow-${arg}`;
+ }, undefined, "result");
+
+ // Assert
+ Assert.equal(result, "arrow-result", "Arrow function should execute correctly");
+ Assert.ok(this._ai, "Test class instance should still be accessible");
+
+ span!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: verify ISpanScope.restore() is called to restore previous active span",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-span");
+ const innerSpan = this._ai.startSpan("inner-span");
+
+ let activeSpanBeforeWithSpan: any = null;
+ let activeSpanInsideWithSpan: any = null;
+ let activeSpanAfterWithSpan: any = null;
+
+ // Act
+ activeSpanBeforeWithSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+
+ withSpan(this._ai.core, innerSpan!, () => {
+ activeSpanInsideWithSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+ Assert.equal(activeSpanInsideWithSpan, innerSpan, "Active span inside withSpan should be inner span");
+ });
+
+ activeSpanAfterWithSpan = this._ai.core.getActiveSpan ? this._ai.core.getActiveSpan() : null;
+
+ // Assert
+ // Active span should be restored after withSpan completes
+ Assert.equal(activeSpanAfterWithSpan, activeSpanBeforeWithSpan,
+ "Active span should be restored after withSpan completes");
+
+ innerSpan!.end();
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: 'this' binding with nested withSpan calls",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-withspan");
+ const innerSpan = this._ai.startSpan("inner-withspan");
+
+ const outerContext = {
+ contextName: "outer",
+ value: 100
+ };
+
+ let outerThisCapture: any = null;
+ let innerThisCapture: any = null;
+ let ai = this._ai;
+
+ // Act - nested withSpan calls with different thisArg
+ withSpan(ai.core, outerSpan!, function(this: typeof outerContext, arg: number) {
+ outerThisCapture = this;
+ Assert.equal(this.contextName, "outer", "Outer 'this' should be outer context");
+ Assert.equal(this.value, 100, "Outer 'this.value' should match");
+
+ withSpan(ai.core, innerSpan!, function(this: ISpanScope) {
+ innerThisCapture = this;
+ // Inner call without explicit thisArg - should be ISpanScope
+ Assert.ok(this.host, "Inner 'this' should be ISpanScope");
+ Assert.ok(this.span, "Inner 'this.span' should exist");
+ Assert.equal(this.span.name, "inner-withspan", "Inner span name should match");
+ });
+
+ return arg * this.value;
+ }, outerContext, 2);
+
+ // Assert
+ Assert.ok(outerThisCapture, "Outer 'this' should be captured");
+ Assert.equal(outerThisCapture.contextName, "outer", "Outer context should be preserved");
+
+ Assert.ok(innerThisCapture, "Inner 'this' should be captured");
+ Assert.ok(innerThisCapture.host, "Inner 'this' should have host");
+ Assert.ok(innerThisCapture.span, "Inner 'this' should have span");
+
+ innerSpan!.end();
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: trace context should match active span context inside withSpan",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("trace-context-match-test", {
+ attributes: {
+ "test.type": "trace-context-validation"
+ }
+ });
+ Assert.ok(testSpan, "Test span should be created");
+
+ let traceCtxInsideWithSpan: any = null;
+ let spanContextInsideWithSpan: any = null;
+ let activeSpanInsideWithSpan: any = null;
+
+ // Act
+ withSpan(this._ai.core, testSpan!, function(this: ISpanScope) {
+ // Get trace context from core
+ traceCtxInsideWithSpan = this.host.getTraceCtx(false);
+
+ // Get span context from the span
+ spanContextInsideWithSpan = this.span.spanContext();
+
+ // Get active span
+ activeSpanInsideWithSpan = this.host.getActiveSpan ? this.host.getActiveSpan() : null;
+ });
+
+ // Assert
+ Assert.ok(traceCtxInsideWithSpan, "Trace context should exist inside withSpan");
+ Assert.ok(spanContextInsideWithSpan, "Span context should exist");
+ Assert.ok(activeSpanInsideWithSpan, "Active span should be set");
+
+ // Verify active span matches the withSpan span
+ Assert.equal(activeSpanInsideWithSpan, testSpan, "Active span should be the withSpan span");
+
+ // Verify trace context matches span context
+ Assert.equal(traceCtxInsideWithSpan.traceId, spanContextInsideWithSpan.traceId,
+ "Trace context traceId should match span context traceId");
+ Assert.equal(traceCtxInsideWithSpan.spanId, spanContextInsideWithSpan.spanId,
+ "Trace context spanId should match span context spanId");
+ Assert.equal(traceCtxInsideWithSpan.traceFlags, spanContextInsideWithSpan.traceFlags,
+ "Trace context traceFlags should match span context traceFlags");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: trace context updates when switching between nested withSpan calls",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-trace-span");
+ const innerSpan = this._ai.startSpan("inner-trace-span");
+
+ let outerTraceCtx: any = null;
+ let outerSpanCtx: any = null;
+ let innerTraceCtx: any = null;
+ let innerSpanCtx: any = null;
+ let ai = this._ai;
+
+ // Act
+ withSpan(ai.core, outerSpan!, function(this: ISpanScope) {
+ outerTraceCtx = this.host.getTraceCtx(false);
+ outerSpanCtx = this.span.spanContext();
+
+ // Verify outer trace context matches outer span
+ Assert.equal(outerTraceCtx.spanId, outerSpanCtx.spanId,
+ "Outer trace context should match outer span");
+
+ // Nested withSpan with different span
+ withSpan(ai.core, innerSpan!, function(this: ISpanScope) {
+ innerTraceCtx = this.host.getTraceCtx(false);
+ innerSpanCtx = this.span.spanContext();
+
+ // Verify inner trace context matches inner span
+ Assert.equal(innerTraceCtx.spanId, innerSpanCtx.spanId,
+ "Inner trace context should match inner span");
+
+ // Verify inner context is different from outer
+ Assert.notEqual(innerTraceCtx.spanId, outerTraceCtx.spanId,
+ "Inner and outer trace contexts should have different spanIds");
+ });
+
+ // After inner withSpan, verify we're back to outer context
+ const restoredTraceCtx = this.host.getTraceCtx(false);
+ Assert.equal(restoredTraceCtx.spanId, outerSpanCtx.spanId,
+ "Trace context should be restored to outer span after inner withSpan completes");
+ });
+
+ outerSpan!.end();
+ innerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: child spans created inside withSpan inherit correct parent context",
+ test: () => {
+ // Arrange
+ const parentSpan = this._ai.startSpan("parent-for-child-test");
+ let ai = this._ai;
+
+ let childSpanContext: any = null;
+ let parentSpanContext: any = null;
+
+ // Act
+ withSpan(ai.core, parentSpan!, function(this: ISpanScope) {
+ parentSpanContext = this.span.spanContext();
+
+ // Create a child span while parent is active
+ const childSpan = ai.startSpan("child-span-in-withSpan");
+ childSpanContext = childSpan!.spanContext();
+
+ // Verify trace context matches parent
+ const traceCtx = this.host.getTraceCtx(false);
+ Assert.equal(traceCtx.spanId, parentSpanContext.spanId,
+ "Trace context should match parent span inside withSpan");
+
+ childSpan!.end();
+ });
+
+ // Assert
+ Assert.ok(childSpanContext, "Child span context should exist");
+ Assert.ok(parentSpanContext, "Parent span context should exist");
+
+ // Child should have same traceId as parent but different spanId
+ Assert.equal(childSpanContext.traceId, parentSpanContext.traceId,
+ "Child span should have same traceId as parent");
+ Assert.notEqual(childSpanContext.spanId, parentSpanContext.spanId,
+ "Child span should have different spanId from parent");
+
+ parentSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: trace context is restored after withSpan completes",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("temporary-trace-span");
+
+ let traceCtxBefore: any = null;
+ let traceCtxInside: any = null;
+ let traceCtxAfter: any = null;
+ let ai = this._ai;
+
+ // Act
+ traceCtxBefore = this._ai.core.getTraceCtx(false);
+
+ withSpan(ai.core, testSpan!, function(this: ISpanScope) {
+ traceCtxInside = this.host.getTraceCtx(false);
+ });
+
+ traceCtxAfter = this._ai.core.getTraceCtx(false);
+
+ // Assert
+ Assert.ok(traceCtxBefore, "Trace context should exist before withSpan (created by startSpan)");
+ Assert.ok(traceCtxInside, "Trace context should exist inside withSpan");
+ Assert.equal(traceCtxInside.spanId, testSpan!.spanContext().spanId,
+ "Trace context inside withSpan should match the test span");
+ Assert.ok(traceCtxAfter, "Trace context should exist after withSpan");
+ Assert.equal(traceCtxAfter.spanId, traceCtxBefore.spanId,
+ "Trace context should be restored to previous state after withSpan");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: trace context reflects parent span when withSpan is nested in another active span",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-active-span");
+
+ this._ai.setActiveSpan(outerSpan!);
+
+ const innerSpan = this._ai.startSpan("inner-withspan-span");
+ let ai = this._ai;
+
+ let outerSpanCtx: any = null;
+ let traceCtxBeforeWithSpan: any = null;
+ let traceCtxInsideWithSpan: any = null;
+ let traceCtxAfterWithSpan: any = null;
+
+ // Act
+ outerSpanCtx = outerSpan!.spanContext();
+ traceCtxBeforeWithSpan = this._ai.core.getTraceCtx(false);
+
+ // Verify initial trace context matches outer span
+ Assert.equal(traceCtxBeforeWithSpan.spanId, outerSpanCtx.spanId,
+ "Trace context should initially match outer span");
+
+ withSpan(ai.core, innerSpan!, function(this: ISpanScope) {
+ traceCtxInsideWithSpan = this.host.getTraceCtx(false);
+ const innerSpanCtx = this.span.spanContext();
+
+ // Inside withSpan, trace context should match inner span
+ Assert.equal(traceCtxInsideWithSpan.spanId, innerSpanCtx.spanId,
+ "Trace context inside withSpan should match inner span");
+ });
+
+ traceCtxAfterWithSpan = this._ai.core.getTraceCtx(false);
+
+ // After withSpan, trace context should be restored to outer span
+ Assert.equal(traceCtxAfterWithSpan.spanId, outerSpanCtx.spanId,
+ "Trace context should be restored to outer span after withSpan");
+
+ innerSpan!.end();
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: trace context traceState is accessible inside withSpan",
+ test: () => {
+ // Arrange
+ const testSpan = this._ai.startSpan("tracestate-test-span");
+ let ai = this._ai;
+
+ let traceStateInside: any = null;
+
+ // Act
+ withSpan(ai.core, testSpan!, function(this: ISpanScope) {
+ const traceCtx = this.host.getTraceCtx(false);
+ traceStateInside = traceCtx ? traceCtx.traceState : null;
+ });
+
+ // Assert
+ Assert.ok(traceStateInside !== undefined,
+ "Trace state should be accessible inside withSpan");
+
+ testSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: span created inside withSpan has parent context matching outer trace context",
+ test: () => {
+ // Arrange
+ const outerSpan = this._ai.startSpan("outer-parent-span");
+
+ this._ai.setActiveSpan(outerSpan!);
+
+ let outerTraceCtx: any = null;
+ let innerSpanParentCtx: any = null;
+ let innerSpanCreated: any = null;
+ let ai = this._ai;
+
+ // Act
+ outerTraceCtx = this._ai.core.getTraceCtx(false);
+
+ withSpan(ai.core, outerSpan!, function(this: ISpanScope) {
+ // Create a new span inside withSpan
+ innerSpanCreated = ai.startSpan("inner-child-span");
+
+ // Get the parent context of the newly created span
+ if (innerSpanCreated) {
+ innerSpanParentCtx = innerSpanCreated.parentSpanContext;
+ }
+
+ innerSpanCreated!.end();
+ });
+
+ // Assert
+ Assert.ok(outerTraceCtx, "Outer trace context should exist");
+ Assert.ok(innerSpanParentCtx, "Inner span should have parent context");
+
+ // Verify parent context matches outer trace context
+ Assert.equal(innerSpanParentCtx.traceId, outerTraceCtx.traceId,
+ "Inner span parent traceId should match outer trace context traceId");
+ Assert.equal(innerSpanParentCtx.spanId, outerTraceCtx.spanId,
+ "Inner span parent spanId should match outer trace context spanId");
+
+ outerSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: span parent context matches trace context when withSpan wraps different span",
+ test: () => {
+ // Arrange - Create initial trace context
+ const contextSpan = this._ai.startSpan("context-span");
+
+ this._ai.setActiveSpan(contextSpan!);
+
+ const contextTraceCtx = this._ai.core.getTraceCtx(false);
+
+ // Create a different span to use in withSpan
+ const wrapperSpan = this._ai.startSpan("wrapper-span");
+
+ let spanCreatedInCallback: any = null;
+ let spanParentCtx: any = null;
+ let ai = this._ai;
+
+ // Act - withSpan with a different span than what's in trace context
+ withSpan(ai.core, wrapperSpan!, function(this: ISpanScope) {
+ // The active span is now wrapperSpan
+ // Create a child span - it should have wrapperSpan as parent
+ spanCreatedInCallback = ai.startSpan("child-of-wrapper");
+
+ if (spanCreatedInCallback) {
+ spanParentCtx = spanCreatedInCallback.parentSpanContext;
+ }
+
+ spanCreatedInCallback!.end();
+ });
+
+ // Assert
+ Assert.ok(spanParentCtx, "Child span should have parent context");
+
+ // Parent should be wrapperSpan (the withSpan span), not contextSpan
+ const wrapperSpanCtx = wrapperSpan!.spanContext();
+ Assert.equal(spanParentCtx.spanId, wrapperSpanCtx.spanId,
+ "Child span parent should be the wrapper span from withSpan");
+ Assert.notEqual(spanParentCtx.spanId, contextTraceCtx.spanId,
+ "Child span parent should NOT be the original context span");
+
+ wrapperSpan!.end();
+ contextSpan!.end();
+ }
+ });
+
+ this.testCase({
+ name: "WithSpan: multiple nested spans maintain correct parent-child relationships with trace context",
+ test: () => {
+ // Arrange
+ const rootSpan = this._ai.startSpan("root-span");
+
+ this._ai.setActiveSpan(rootSpan!);
+
+ const level1Span = this._ai.startSpan("level1-span");
+
+ let level2SpanParent: any = null;
+ let level2SpanCtx: any = null;
+ let level3SpanParent: any = null;
+ let ai = this._ai;
+
+ // Act - Nested withSpan calls
+ withSpan(ai.core, level1Span!, function(this: ISpanScope) {
+ const level1TraceCtx = this.host.getTraceCtx(false);
+
+ // Create level2 span - should have level1 as parent
+ const level2Span = ai.startSpan("level2-span");
+ if (level2Span) {
+ level2SpanParent = level2Span.parentSpanContext;
+ level2SpanCtx = level2Span.spanContext();
+ }
+
+ withSpan(ai.core, level2Span!, function(this: ISpanScope) {
+ const level2TraceCtx = this.host.getTraceCtx(false);
+
+ // Create level3 span - should have level2 as parent
+ const level3Span = ai.startSpan("level3-span");
+ if (level3Span) {
+ level3SpanParent = level3Span.parentSpanContext;
+ }
+
+ // Verify level3 parent matches level2 trace context
+ Assert.equal(level3SpanParent.spanId, level2TraceCtx.spanId,
+ "Level3 span parent should match level2 trace context");
+
+ level3Span!.end();
+ });
+
+ // Verify level2 parent matches level1 trace context
+ Assert.equal(level2SpanParent.spanId, level1TraceCtx.spanId,
+ "Level2 span parent should match level1 trace context");
+
+ level2Span!.end();
+ });
+
+ // Assert
+ Assert.ok(level2SpanParent, "Level2 span should have parent context");
+ Assert.ok(level2SpanCtx, "Level2 span context should exist");
+ Assert.ok(level3SpanParent, "Level3 span should have parent context");
+
+ // Verify the chain: root -> level1 -> level2 -> level3
+ Assert.equal(level2SpanParent.spanId, level1Span!.spanContext().spanId,
+ "Level2 parent should be level1");
+ Assert.equal(level3SpanParent.spanId, level2SpanCtx.spanId,
+ "Level3 parent should be level2");
+
+ level1Span!.end();
+ rootSpan!.end();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/AISKU/Tests/Unit/src/aiskuunittests.ts b/AISKU/Tests/Unit/src/aiskuunittests.ts
index 8a53729b0..3088c034d 100644
--- a/AISKU/Tests/Unit/src/aiskuunittests.ts
+++ b/AISKU/Tests/Unit/src/aiskuunittests.ts
@@ -1,17 +1,44 @@
import { AISKUSizeCheck } from "./AISKUSize.Tests";
-import { ApplicationInsightsTests } from './applicationinsights.e2e.tests';
-import { ApplicationInsightsFetchTests } from './applicationinsights.e2e.fetch.tests';
-import { CdnPackagingChecks } from './CdnPackaging.tests';
-import { GlobalTestHooks } from './GlobalTestHooks.Test';
-import { SanitizerE2ETests } from './sanitizer.e2e.tests';
-import { ValidateE2ETests } from './validate.e2e.tests';
-import { SenderE2ETests } from './sender.e2e.tests';
-import { SnippetInitializationTests } from './SnippetInitialization.Tests';
+import { ApplicationInsightsTests } from "./applicationinsights.e2e.tests";
+import { ApplicationInsightsFetchTests } from "./applicationinsights.e2e.fetch.tests";
+import { CdnPackagingChecks } from "./CdnPackaging.tests";
+import { GlobalTestHooks } from "./GlobalTestHooks.Test";
+import { SanitizerE2ETests } from "./sanitizer.e2e.tests";
+import { ValidateE2ETests } from "./validate.e2e.tests";
+import { SenderE2ETests } from "./sender.e2e.tests";
+import { SnippetInitializationTests } from "./SnippetInitialization.Tests";
import { CdnThrottle} from "./CdnThrottle.tests";
import { ThrottleSentMessage } from "./ThrottleSentMessage.tests";
-import { IAnalyticsConfigTests } from './IAnalyticsConfig.Tests';
+import { IAnalyticsConfigTests } from "./IAnalyticsConfig.Tests";
+import { StartSpanTests } from "./StartSpan.Tests";
+import { UseSpanTests } from "./UseSpan.Tests";
+import { WithSpanTests } from "./WithSpan.Tests";
+import { SpanContextPropagationTests } from "./SpanContextPropagation.Tests";
+import { SpanLifeCycleTests } from "./SpanLifeCycle.Tests"
+import { TelemetryItemGenerationTests } from "./TelemetryItemGeneration.Tests";
+import { SpanErrorHandlingTests } from "./SpanErrorHandling.Tests";
+import { SpanUtilsTests } from "./SpanUtils.Tests";
+import { SpanE2ETests } from "./SpanE2E.Tests";
+import { NonRecordingSpanTests } from "./NonRecordingSpan.Tests";
+import { SpanPluginIntegrationTests } from "./SpanPluginIntegration.Tests";
+import { SpanHelperUtilsTests } from "./SpanHelperUtils.Tests";
+import { TraceSuppressionTests } from "./TraceSuppression.Tests";
+import { TraceProviderTests } from "./TraceProvider.Tests";
+import { TraceContextTests } from "./TraceContext.Tests";
+import { OTelInitTests } from "./OTelInit.Tests";
export function runTests() {
+ new OTelInitTests().registerTests();
+ new TraceSuppressionTests().registerTests();
+ new SpanErrorHandlingTests().registerTests();
+ new SpanUtilsTests().registerTests();
+ new SpanE2ETests().registerTests();
+ new NonRecordingSpanTests().registerTests();
+ new SpanPluginIntegrationTests().registerTests();
+ new SpanHelperUtilsTests().registerTests();
+ new TraceProviderTests().registerTests();
+ new TraceContextTests().registerTests();
+
new GlobalTestHooks().registerTests();
new AISKUSizeCheck().registerTests();
new ApplicationInsightsTests().registerTests();
@@ -25,4 +52,10 @@ export function runTests() {
new ThrottleSentMessage().registerTests();
new CdnThrottle().registerTests();
new IAnalyticsConfigTests().registerTests();
+ new StartSpanTests().registerTests();
+ new WithSpanTests().registerTests();
+ new UseSpanTests().registerTests();
+ new SpanContextPropagationTests().registerTests();
+ new SpanLifeCycleTests().registerTests();
+ new TelemetryItemGenerationTests().registerTests();
}
\ No newline at end of file
diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.fetch.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.fetch.tests.ts
index 3d5a044a5..a2c601760 100644
--- a/AISKU/Tests/Unit/src/applicationinsights.e2e.fetch.tests.ts
+++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.fetch.tests.ts
@@ -1,4 +1,4 @@
-import { DistributedTracingModes, IConfig } from '@microsoft/applicationinsights-common';
+import { DistributedTracingModes, IConfig } from '@microsoft/applicationinsights-core-js';
import { ApplicationInsightsTests } from './applicationinsights.e2e.tests';
import { IConfiguration } from '@microsoft/applicationinsights-core-js';
diff --git a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts
index a607c3b42..490a72de2 100644
--- a/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts
+++ b/AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts
@@ -2,21 +2,21 @@ import { AITestClass, Assert, PollingAssert, EventValidator, TraceValidator, Exc
import { SinonSpy } from 'sinon';
import { ApplicationInsights } from '../../../src/applicationinsights-web'
import { Sender } from '@microsoft/applicationinsights-channel-js';
-import { IDependencyTelemetry, ContextTagKeys, Event, Trace, Exception, Metric, PageView, PageViewPerformance, RemoteDependencyData, DistributedTracingModes, RequestHeaders, IAutoExceptionTelemetry, BreezeChannelIdentifier, IConfig, EventPersistence } from '@microsoft/applicationinsights-common';
+import { IDependencyTelemetry, ContextTagKeys, Exception, DistributedTracingModes, RequestHeaders, IAutoExceptionTelemetry, BreezeChannelIdentifier, IConfig, EventPersistence, EventDataType, PageViewDataType, TraceDataType, ExceptionDataType, MetricDataType, PageViewPerformanceDataType, RemoteDependencyDataType } from '@microsoft/applicationinsights-core-js';
import { ITelemetryItem, getGlobal, newId, dumpObj, BaseTelemetryPlugin, IProcessTelemetryContext, __getRegisteredEvents, arrForEach, IConfiguration, ActiveStatus, FeatureOptInMode } from "@microsoft/applicationinsights-core-js";
-import { TelemetryContext } from '@microsoft/applicationinsights-properties-js';
+import { IPropTelemetryContext } from '@microsoft/applicationinsights-properties-js';
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 { IStackFrame } from '@microsoft/applicationinsights-core-js';
import { utcNow } from '@nevware21/ts-utils';
-function _checkExpectedFrame(expectedFrame: IStackFrame, actualFrame: IStackFrame, index: number) {
+function _checkExpectedFrame(expectedFrame: IStackFrame, actualFrame: IStackFrame, index: number) {
Assert.equal(expectedFrame.assembly, actualFrame.assembly, index + ") Assembly is not as expected");
Assert.equal(expectedFrame.fileName, actualFrame.fileName, index + ") FileName is not as expected");
Assert.equal(expectedFrame.line, actualFrame.line, index + ") Line is not as expected");
Assert.equal(expectedFrame.method, actualFrame.method, index + ") Method is not as expected");
- Assert.equal(expectedFrame.level, actualFrame.level, index + ") Level is not as expected");
+ Assert.equal(expectedFrame.level, actualFrame.level, index + ") Level is not as expected");
}
export class ApplicationInsightsTests extends AITestClass {
@@ -40,7 +40,7 @@ export class ApplicationInsightsTests extends AITestClass {
private _ai: ApplicationInsights;
private _aiName: string = 'AppInsightsSDK';
- private isFetchPolyfill:boolean = false;
+ private isFetchPolyfill: boolean = false;
// Sinon
private errorSpy: SinonSpy;
@@ -61,7 +61,7 @@ export class ApplicationInsightsTests extends AITestClass {
constructor(testName?: string) {
super(testName || "ApplicationInsightsTests");
}
-
+
protected _getTestConfig(sessionPrefix: string) {
let config: IConfiguration | IConfig = {
connectionString: ApplicationInsightsTests._connectionString,
@@ -76,7 +76,7 @@ export class ApplicationInsightsTests extends AITestClass {
distributedTracingMode: DistributedTracingModes.AI_AND_W3C,
samplingPercentage: 50,
convertUndefined: "test-value",
- disablePageUnloadEvents: [ "beforeunload" ],
+ disablePageUnloadEvents: ["beforeunload"],
extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
//cfgUrl: ""
@@ -229,7 +229,7 @@ export class ApplicationInsightsTests extends AITestClass {
let onChangeCalled = 0;
let handler = this._ai.onCfgChange((details) => {
- onChangeCalled ++;
+ onChangeCalled++;
Assert.equal(expectedIkey, details.cfg.instrumentationKey, "Expect the iKey to be set");
Assert.equal(expectedEndpointUrl, details.cfg.endpointUrl, "Expect the endpoint to be set");
Assert.equal(expectedLoggingLevel, details.cfg.diagnosticLogInterval, "Expect the diagnosticLogInterval to be set");
@@ -256,7 +256,7 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.equal(3, onChangeCalled, "Expected the onChanged was called");
this.clock.tick(1);
Assert.equal(4, onChangeCalled, "Expected the onChanged was called again");
-
+
// Remove the handler
handler.rm();
}
@@ -274,15 +274,15 @@ export class ApplicationInsightsTests extends AITestClass {
// force unload
oriInst.unload(false);
}
-
+
if (oriInst && oriInst["dependencies"]) {
oriInst["dependencies"].teardown();
}
-
+
this._config = this._getTestConfig(this._sessionPrefix);
let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl");
this._config.connectionString = csPromise;
- this._config.initTimeOut= 80000;
+ this._config.initTimeOut = 80000;
this._ctx.csPromise = csPromise;
@@ -295,17 +295,17 @@ export class ApplicationInsightsTests extends AITestClass {
let core = this._ai.core;
let status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending");
-
-
+
+
}].concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
let csPromise = this._ctx.csPromise;
let config = this._ai.config;
-
+
if (csPromise.state === "resolved" && activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set");
- Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set");
+ Assert.equal("testUrl/v2/track", core.config.endpointUrl, "endpoint shoule be set");
config.connectionString = "InstrumentationKey=testIkey1;ingestionendpoint=testUrl1";
this.clock.tick(1);
@@ -318,10 +318,10 @@ export class ApplicationInsightsTests extends AITestClass {
}, "Wait for promise response" + new Date().toISOString(), 60) as any).concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
-
+
if (activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey1", core.config.instrumentationKey, "ikey should be set test1");
- Assert.equal("testUrl1/v2/track", core.config.endpointUrl ,"endpoint shoule be set test1");
+ Assert.equal("testUrl1/v2/track", core.config.endpointUrl, "endpoint shoule be set test1");
return true;
}
return false;
@@ -340,15 +340,15 @@ export class ApplicationInsightsTests extends AITestClass {
// force unload
oriInst.unload(false);
}
-
+
if (oriInst && oriInst["dependencies"]) {
oriInst["dependencies"].teardown();
}
-
+
this._config = this._getTestConfig(this._sessionPrefix);
let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl");
this._config.connectionString = csPromise;
- this._config.initTimeOut= 80000;
+ this._config.initTimeOut = 80000;
this._ctx.csPromise = csPromise;
@@ -361,22 +361,22 @@ export class ApplicationInsightsTests extends AITestClass {
let core = this._ai.core;
let status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending");
-
+
config.connectionString = "InstrumentationKey=testIkey1;ingestionendpoint=testUrl1";
this.clock.tick(1);
status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.ACTIVE, "active status should be set to active in next executing cycle");
// Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending test1");
-
-
+
+
}].concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
-
+
if (activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set");
- Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set");
+ Assert.equal("testUrl/v2/track", core.config.endpointUrl, "endpoint shoule be set");
return true;
}
return false;
@@ -396,17 +396,17 @@ export class ApplicationInsightsTests extends AITestClass {
// force unload
oriInst.unload(false);
}
-
+
if (oriInst && oriInst["dependencies"]) {
oriInst["dependencies"].teardown();
}
-
+
this._config = this._getTestConfig(this._sessionPrefix);
let csPromise = createAsyncResolvedPromise("InstrumentationKey=testIkey;ingestionendpoint=testUrl");
this._config.connectionString = csPromise;
let offlineChannel = new OfflineChannel();
this._config.channels = [[offlineChannel]];
- this._config.initTimeOut= 80000;
+ this._config.initTimeOut = 80000;
let init = new ApplicationInsights({
@@ -419,21 +419,21 @@ export class ApplicationInsightsTests extends AITestClass {
let status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending");
-
+
config.connectionString = "InstrumentationKey=testIkey1;ingestionendpoint=testUrl1"
this.clock.tick(1);
status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.ACTIVE, "active status should be set to active in next executing cycle");
// Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending test1");
-
-
+
+
}].concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
-
+
if (activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set");
- Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set");
+ Assert.equal("testUrl/v2/track", core.config.endpointUrl, "endpoint shoule be set");
let sendChannel = this._ai.getPlugin(BreezeChannelIdentifier);
let offlineChannelPlugin = this._ai.getPlugin("OfflineChannel").plugin;
Assert.equal(sendChannel.plugin.isInitialized(), true, "sender is initialized");
@@ -447,7 +447,7 @@ export class ApplicationInsightsTests extends AITestClass {
});
-
+
this.testCaseAsync({
name: "Init: init with cs string, change with cs promise",
stepDelay: 100,
@@ -472,15 +472,15 @@ export class ApplicationInsightsTests extends AITestClass {
status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.ACTIVE, "active status should be set to active in next executing cycle");
//Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending");
-
-
+
+
}].concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
-
+
if (activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set");
- Assert.equal("testUrl/v2/track", core.config.endpointUrl ,"endpoint shoule be set");
+ Assert.equal("testUrl/v2/track", core.config.endpointUrl, "endpoint shoule be set");
return true;
}
return false;
@@ -499,11 +499,11 @@ export class ApplicationInsightsTests extends AITestClass {
// force unload
oriInst.unload(false);
}
-
+
if (oriInst && oriInst["dependencies"]) {
oriInst["dependencies"].teardown();
}
-
+
this._config = this._getTestConfig(this._sessionPrefix);
let ikeyPromise = createAsyncResolvedPromise("testIkey");
let endpointPromise = createAsyncResolvedPromise("testUrl");
@@ -512,7 +512,7 @@ export class ApplicationInsightsTests extends AITestClass {
this._config.connectionString = null;
this._config.instrumentationKey = ikeyPromise;
this._config.endpointUrl = endpointPromise;
- this._config.initTimeOut= 80000;
+ this._config.initTimeOut = 80000;
@@ -525,16 +525,16 @@ export class ApplicationInsightsTests extends AITestClass {
let core = this._ai.core;
let status = core.activeStatus && core.activeStatus();
Assert.equal(status, ActiveStatus.PENDING, "status should be set to pending");
- Assert.equal(config.connectionString,null, "connection string shoule be null");
-
-
+ Assert.equal(config.connectionString, null, "connection string shoule be null");
+
+
}].concat(PollingAssert.createPollingAssert(() => {
let core = this._ai.core
let activeStatus = core.activeStatus && core.activeStatus();
-
+
if (activeStatus === ActiveStatus.ACTIVE) {
Assert.equal("testIkey", core.config.instrumentationKey, "ikey should be set");
- Assert.equal("testUrl", core.config.endpointUrl ,"endpoint shoule be set");
+ Assert.equal("testUrl", core.config.endpointUrl, "endpoint shoule be set");
return true;
}
return false;
@@ -548,21 +548,21 @@ export class ApplicationInsightsTests extends AITestClass {
test: () => {
let fetchcalled = 0;
let overrideFetchFn = (url: string, oncomplete: any, isAutoSync?: boolean) => {
- fetchcalled ++;
+ fetchcalled++;
Assert.equal(url, CONFIG_ENDPOINT_URL, "fetch should be called with prod cdn");
};
let config = {
instrumentationKey: "testIKey",
- extensionConfig:{
+ extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
overrideFetchFn: overrideFetchFn
}
}
} as IConfiguration & IConfig;
- let ai = new ApplicationInsights({config: config});
+ let ai = new ApplicationInsights({ config: config });
ai.loadAppInsights();
-
+
ai.config.extensionConfig = ai.config.extensionConfig || {};
let extConfig = ai.config.extensionConfig["AppInsightsCfgSyncPlugin"];
Assert.equal(extConfig.cfgUrl, CONFIG_ENDPOINT_URL, "default cdn endpoint should be set");
@@ -570,7 +570,7 @@ export class ApplicationInsightsTests extends AITestClass {
let featureOptIn = config.featureOptIn || {};
Assert.equal(featureOptIn["iKeyUsage"].mode, FeatureOptInMode.enable, "ikey message should be turned on");
-
+
Assert.equal(fetchcalled, 1, "fetch should be called once");
config.extensionConfig = config.extensionConfig || {};
let expectedTimeout = 2000000000;
@@ -596,15 +596,15 @@ export class ApplicationInsightsTests extends AITestClass {
let config = {
instrumentationKey: "testIKey",
endpointUrl: "testUrl",
- extensionConfig:{
+ extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
cfgUrl: ""
}
},
- extensions:[offlineChannel]
+ extensions: [offlineChannel]
} as IConfiguration & IConfig;
- let ai = new ApplicationInsights({config: config});
+ let ai = new ApplicationInsights({ config: config });
ai.loadAppInsights();
this.clock.tick(1);
@@ -620,7 +620,7 @@ export class ApplicationInsightsTests extends AITestClass {
ai["dependencies"].teardown();
}
//offlineChannel.teardown();
-
+
}
});
@@ -633,15 +633,15 @@ export class ApplicationInsightsTests extends AITestClass {
let config = {
instrumentationKey: "testIKey",
endpointUrl: "testUrl",
- extensionConfig:{
+ extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
cfgUrl: ""
}
},
- channels:[[offlineChannel]]
+ channels: [[offlineChannel]]
} as IConfiguration & IConfig;
- let ai = new ApplicationInsights({config: config});
+ let ai = new ApplicationInsights({ config: config });
ai.loadAppInsights();
this.clock.tick(1);
@@ -651,13 +651,13 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.equal(offlineChannelPlugin.isInitialized(), true, "offline channel is initialized");
let urlConfig = offlineChannelPlugin["_getDbgPlgTargets"]()[0];
Assert.ok(urlConfig, "offline url config is initialized");
-
+
ai.unload(false);
if (ai && ai["dependencies"]) {
ai["dependencies"].teardown();
}
-
+
}
});
@@ -669,15 +669,15 @@ export class ApplicationInsightsTests extends AITestClass {
let offlineChannel = new OfflineChannel();
let config = {
connectionString: "InstrumentationKey=testIKey",
- extensionConfig:{
+ extensionConfig: {
["AppInsightsCfgSyncPlugin"]: {
cfgUrl: ""
}
},
- channels:[[offlineChannel]]
+ channels: [[offlineChannel]]
} as IConfiguration & IConfig;
- let ai = new ApplicationInsights({config: config});
+ let ai = new ApplicationInsights({ config: config });
ai.loadAppInsights();
this.clock.tick(1);
@@ -696,7 +696,7 @@ export class ApplicationInsightsTests extends AITestClass {
}
}
});
-
+
}
public addCDNOverrideTests(): void {
@@ -766,15 +766,15 @@ export class ApplicationInsightsTests extends AITestClass {
headers.forEach((val, key) => {
if (key === "content-type") {
Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header");
- headerCnt ++;
+ headerCnt++;
}
if (key === "x-ms-meta-aijssdksrc") {
Assert.ok(val, "should have sdk src response header");
- headerCnt ++;
+ headerCnt++;
}
if (key === "x-ms-meta-aijssdkver") {
Assert.ok(val, "should have version number for response header");
- headerCnt ++;
+ headerCnt++;
}
});
Assert.equal(headerCnt, 3, "all expected headers should be present");
@@ -819,15 +819,15 @@ export class ApplicationInsightsTests extends AITestClass {
headers.forEach((val, key) => {
if (key === "content-type") {
Assert.deepEqual(val, "text/javascript; charset=utf-8", "should have correct content-type response header");
- headerCnt ++;
+ headerCnt++;
}
if (key === "x-ms-meta-aijssdksrc") {
Assert.ok(val, "should have sdk src response header");
- headerCnt ++;
+ headerCnt++;
}
if (key === "x-ms-meta-aijssdkver") {
Assert.ok(val, "should have version number for response header");
- headerCnt ++;
+ headerCnt++;
}
});
Assert.equal(headerCnt, 3, "all expected headers should be present");
@@ -855,7 +855,60 @@ export class ApplicationInsightsTests extends AITestClass {
if (res.ok) {
let val = await res.text();
- Assert.ok(val, "Response text should be returned" );
+ Assert.ok(val, "Response text should be returned");
+ } else {
+ Assert.fail("Fetch failed with status: " + dumpObj(res));
+ }
+ } catch (e) {
+ Assert.fail("Fetch Error: " + dumpObj(e));
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E.GenericTests: Fetch Static Web js1 - CDN V3",
+ useFakeServer: false,
+ useFakeFetch: false,
+ fakeFetchAutoRespond: false,
+ test: async () => {
+ // 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 {
+ let res = await fetch(`https://js1.tst.applicationinsights.io/scripts/b/ai.3.gbl.min.js?${random}`, {
+ method: "GET"
+ });
+
+ if (res.ok) {
+ let val = await res.text();
+ Assert.ok(val, "Response text should be returned");
+ } else {
+ Assert.fail("Fetch failed with status: " + dumpObj(res));
+ }
+ } catch (e) {
+ this._ctx.err = e;
+ Assert.fail("Fetch Error: " + dumpObj(e));
+ }
+ }
+ });
+
+ this.testCase({
+ name: "E2E.GenericTests: Fetch Static Web js2 - CDN V3",
+ useFakeServer: false,
+ useFakeFetch: false,
+ fakeFetchAutoRespond: false,
+ test: async () => {
+ // 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 {
+ let res = await fetch(`https://js2.tst.applicationinsights.io/scripts/b/ai.3.gbl.min.js?${random}`, {
+ method: "GET"
+ });
+
+ if (res.ok) {
+ let val = await res.text();
+ Assert.ok(val, "Response text should be returned");
} else {
Assert.fail("Fetch failed with status: " + dumpObj(res));
}
@@ -906,8 +959,8 @@ export class ApplicationInsightsTests extends AITestClass {
if (payloadStr.length > 0) {
const payload = JSON.parse(payloadStr[0]);
const data = payload.data;
- Assert.ok( payload && payload.iKey);
- Assert.equal( ApplicationInsightsTests._instrumentationKey,payload.iKey,"payload ikey is not set correctly" );
+ Assert.ok(payload && payload.iKey);
+ Assert.equal(ApplicationInsightsTests._instrumentationKey, payload.iKey, "payload ikey is not set correctly");
Assert.ok(data && data.baseData && data.baseData.properties["prop1"]);
Assert.ok(data && data.baseData && data.baseData.measurements["measurement1"]);
}
@@ -923,8 +976,8 @@ export class ApplicationInsightsTests extends AITestClass {
if (payloadStr.length > 0) {
const payload = JSON.parse(payloadStr[0]);
const data = payload.data;
- Assert.ok( payload && payload.iKey);
- Assert.equal( ApplicationInsightsTests._instrumentationKey,payload.iKey,"payload ikey is not set correctly" );
+ Assert.ok(payload && payload.iKey);
+ Assert.equal(ApplicationInsightsTests._instrumentationKey, payload.iKey, "payload ikey is not set correctly");
Assert.ok(data && data.baseData && data.baseData.properties["prop1"]);
Assert.ok(data && data.baseData && data.baseData.measurements["measurement1"]);
}
@@ -981,7 +1034,7 @@ export class ApplicationInsightsTests extends AITestClass {
error: e,
evt: null
} as IAutoExceptionTelemetry;
-
+
exception = e;
this._ai.trackException({ exception: autoTelemetry });
}
@@ -1007,7 +1060,7 @@ export class ApplicationInsightsTests extends AITestClass {
error: e,
evt: null
} as IAutoExceptionTelemetry;
-
+
exception = e;
this._ai.trackException({ exception: autoTelemetry }, { custom: "custom value" });
}
@@ -1033,7 +1086,7 @@ export class ApplicationInsightsTests extends AITestClass {
error: e.toString(),
evt: null
} as IAutoExceptionTelemetry;
-
+
exception = e;
this._ai.trackException({ exception: autoTelemetry });
}
@@ -1059,7 +1112,7 @@ export class ApplicationInsightsTests extends AITestClass {
error: undefined,
evt: null
} as IAutoExceptionTelemetry;
-
+
try {
exception = e;
this._ai.trackException({ exception: autoTelemetry });
@@ -1091,7 +1144,7 @@ export class ApplicationInsightsTests extends AITestClass {
error: undefined,
evt: null
} as IAutoExceptionTelemetry;
-
+
try {
exception = e;
this._ai.trackException({ exception: autoTelemetry }, { custom: "custom value" });
@@ -1155,7 +1208,7 @@ export class ApplicationInsightsTests extends AITestClass {
name: 'E2E.GenericTests: trackException will keep id from the original exception',
stepDelay: 1,
steps: [() => {
- this._ai.trackException({id:"testId", error: new Error("test local exception"), severityLevel: 3});
+ this._ai.trackException({ id: "testId", error: new Error("test local exception"), severityLevel: 3 });
}].concat(this.asserts(1)).concat(() => {
const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
if (payloadStr.length > 0) {
@@ -1213,7 +1266,7 @@ export class ApplicationInsightsTests extends AITestClass {
steps: [() => {
let errObj = {
name: "E2E.GenericTests",
- reason:{
+ reason: {
message: "Test_Error_Throwing_Inside_UseCallback",
stack: "Error: Test_Error_Throwing_Inside_UseCallback\n" +
"at http://localhost:3000/static/js/main.206f4846.js:2:296748\n" + // Anonymous function with no function name attribution (firefox/ios)
@@ -1233,7 +1286,7 @@ export class ApplicationInsightsTests extends AITestClass {
" Line 11 of inline#1 script in http://localhost:3000/static/js/main.206f4846.js:2:296748\n" + // With Line 11 of inline#1 script attribution
" Line 68 of inline#2 script in file://localhost/teststack.html\n" + // With Line 68 of inline#2 script attribution
"at Function.Module._load (module.js:407:3)\n" +
- " at Function.Module.runMain (module.js:575:10)\n"+
+ " at Function.Module.runMain (module.js:575:10)\n" +
" at startup (node.js:159:18)\n" +
"at Global code (http://example.com/stacktrace.js:11:1)\n" +
"at Object.Module._extensions..js (module.js:550:10)\n" +
@@ -1306,7 +1359,7 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.equal(ex.parsedStack.length, 29);
for (let lp = 0; lp < ex.parsedStack.length; lp++) {
_checkExpectedFrame(expectedParsedStack[lp], ex.parsedStack[lp], lp);
- }
+ }
Assert.ok(baseData.properties, "Has BaseData properties");
Assert.equal(baseData.properties.custom, "custom value");
@@ -1322,13 +1375,13 @@ export class ApplicationInsightsTests extends AITestClass {
stepDelay: 1,
steps: [() => {
let message = "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n" +
- "1. You might have mismatching versions of React and the renderer (such as React DOM)\n" +
- "2. You might be breaking the Rules of Hooks\n" +
- "3. You might have more than one copy of React in the same app\n" +
- "See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.";
+ "1. You might have mismatching versions of React and the renderer (such as React DOM)\n" +
+ "2. You might be breaking the Rules of Hooks\n" +
+ "3. You might have more than one copy of React in the same app\n" +
+ "See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.";
let errObj = {
typeName: "Error",
- reason:{
+ reason: {
message: "Error: " + message,
stack: "Error: " + message + "\n" +
" at Object.throwInvalidHookError (https://localhost:44365/static/js/bundle.js:201419:13)\n" +
@@ -1383,7 +1436,7 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.equal(ex.parsedStack.length, 10);
for (let lp = 0; lp < ex.parsedStack.length; lp++) {
_checkExpectedFrame(expectedParsedStack[lp], ex.parsedStack[lp], lp);
- }
+ }
Assert.ok(baseData.properties, "Has BaseData properties");
Assert.equal(baseData.properties.custom, "custom value");
@@ -1401,7 +1454,7 @@ export class ApplicationInsightsTests extends AITestClass {
() => {
console.log("* calling trackMetric " + new Date().toISOString());
for (let i = 0; i < 100; i++) {
- this._ai.trackMetric({ name: "test" + i, average: Math.round(100 * Math.random()), min: 1, max: i+1, stdDev: 10.0 * Math.random() });
+ this._ai.trackMetric({ name: "test" + i, average: Math.round(100 * Math.random()), min: 1, max: i + 1, stdDev: 10.0 * Math.random() });
}
console.log("* done calling trackMetric " + new Date().toISOString());
}
@@ -1431,21 +1484,21 @@ export class ApplicationInsightsTests extends AITestClass {
this._ai.trackPageView(); // sends 2
}
]
- .concat(this.asserts(2))
- .concat(() => {
+ .concat(this.asserts(2))
+ .concat(() => {
- const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
- if (payloadStr.length > 0) {
- const payload = JSON.parse(payloadStr[0]);
- const data = payload.data;
- Assert.ok(data.baseData.id, "pageView id is defined");
- Assert.ok(data.baseData.id.length > 0);
- Assert.ok(payload.tags["ai.operation.id"]);
- Assert.equal(data.baseData.id, payload.tags["ai.operation.id"], "pageView id matches current operation id");
- } else {
- Assert.ok(false, "successSpy not called");
- }
- })
+ const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
+ if (payloadStr.length > 0) {
+ const payload = JSON.parse(payloadStr[0]);
+ const data = payload.data;
+ Assert.ok(data.baseData.id, "pageView id is defined");
+ Assert.ok(data.baseData.id.length > 0);
+ Assert.ok(payload.tags["ai.operation.id"]);
+ Assert.equal(data.baseData.id, payload.tags["ai.operation.id"], "pageView id matches current operation id");
+ } else {
+ Assert.ok(false, "successSpy not called");
+ }
+ })
});
this.testCaseAsync({
@@ -1545,20 +1598,20 @@ export class ApplicationInsightsTests extends AITestClass {
name: 'E2E.GenericTests: undefined properties are replaced by customer defined value with config convertUndefined.',
stepDelay: 1,
steps: [() => {
- this._ai.trackPageView({ name: 'pageview', properties: { 'prop1': 'val1' }});
+ this._ai.trackPageView({ name: 'pageview', properties: { 'prop1': 'val1' } });
this._ai.trackEvent({ name: 'event', properties: { 'prop2': undefined } });
}].concat(this.asserts(3)).concat(() => {
const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
for (let i = 0; i < payloadStr.length; i++) {
- const payload = JSON.parse(payloadStr[i]);const baseType = payload.data.baseType;
+ const payload = JSON.parse(payloadStr[i]); const baseType = payload.data.baseType;
// Make the appropriate assersion depending on the baseType
switch (baseType) {
- case Event.dataType:
+ case EventDataType:
const eventData = payload.data;
Assert.ok(eventData && eventData.baseData && eventData.baseData.properties['prop2']);
Assert.equal(eventData.baseData.properties['prop2'], 'test-value');
break;
- case PageView.dataType:
+ case PageViewDataType:
const pageViewData = payload.data;
Assert.ok(pageViewData && pageViewData.baseData && pageViewData.baseData.properties['prop1']);
Assert.equal(pageViewData.baseData.properties['prop1'], 'val1');
@@ -1597,7 +1650,7 @@ export class ApplicationInsightsTests extends AITestClass {
steps: [
() => {
const xhr = new XMLHttpRequest();
- xhr.open('GET', 'https://httpbin.org/status/200');
+ xhr.open('GET', 'https://localhost:9001/AISKU');
xhr.send();
Assert.ok(true);
}
@@ -1612,22 +1665,22 @@ export class ApplicationInsightsTests extends AITestClass {
fakeFetchAutoRespond: true,
steps: [
() => {
- fetch('https://httpbin.org/status/200', { method: 'GET', headers: { 'header': 'value'} });
+ fetch('http://localhost:9001/README.md', { method: 'GET', headers: { 'header': 'value' } });
Assert.ok(true, "fetch monitoring is instrumented");
},
() => {
- fetch('https://httpbin.org/status/200', { method: 'GET' });
+ fetch('http://localhost:9001/README.md', { method: 'GET' });
Assert.ok(true, "fetch monitoring is instrumented");
},
() => {
- fetch('https://httpbin.org/status/200');
+ fetch('http://localhost:9001/README.md');
Assert.ok(true, "fetch monitoring is instrumented");
}
].concat(this.asserts(3, false, false))
.concat(() => {
let args = [];
this.trackSpy.args.forEach(call => {
- let message = call[0].baseData.message||"";
+ let message = call[0].baseData.message || "";
// Ignore the internal SendBrowserInfoOnUserInit message (Only occurs when running tests in a browser)
if (message.indexOf("AI (Internal): 72 ") == -1) {
args.push(call[0]);
@@ -1651,6 +1704,7 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.requestIdHeader], "Request-Id header");
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.requestContextHeader], "Request-Context header");
Assert.ok(baseData.properties.requestHeaders[RequestHeaders.traceParentHeader], "traceparent");
+ Assert.ok(!baseData.properties.requestHeaders[RequestHeaders.traceStateHeader], "traceState should not be present in outbound event");
const id: string = baseData.id;
const regex = id.match(/\|.{32}\..{16}\./g);
Assert.ok(id.length > 0);
@@ -1680,23 +1734,23 @@ export class ApplicationInsightsTests extends AITestClass {
this._ai.trackEvent({ name: "Custom event via addTelemetryInitializer" });
}
]
- .concat(this.asserts(1, false, false))
- .concat(PollingAssert.createPollingAssert(() => {
- const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
- if (payloadStr.length) {
- const payload = JSON.parse(payloadStr[0]);
+ .concat(this.asserts(1, false, false))
+ .concat(PollingAssert.createPollingAssert(() => {
+ const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
+ if (payloadStr.length) {
+ const payload = JSON.parse(payloadStr[0]);
Assert.equal(1, payloadStr.length, 'Only 1 track item is sent - ' + payload.name);
Assert.ok(payload);
- if (payload && payload.tags) {
- const tagResult: string = payload.tags && payload.tags[this.tagKeys.cloudName];
- const tagExpect: string = 'my.custom.cloud.name';
- Assert.equal(tagResult, tagExpect, 'telemetryinitializer tag override successful');
- return true;
+ if (payload && payload.tags) {
+ const tagResult: string = payload.tags && payload.tags[this.tagKeys.cloudName];
+ const tagExpect: string = 'my.custom.cloud.name';
+ Assert.equal(tagResult, tagExpect, 'telemetryinitializer tag override successful');
+ return true;
+ }
+ return false;
}
- return false;
- }
- }, 'Set custom tags') as any)
+ }, 'Set custom tags') as any)
});
this.testCaseAsync({
@@ -1705,28 +1759,28 @@ export class ApplicationInsightsTests extends AITestClass {
steps: [
() => {
this._ai.addTelemetryInitializer((item: ITelemetryItem) => {
- item.tags.push({[this.tagKeys.cloudName]: "my.shim.cloud.name"});
+ item.tags.push({ [this.tagKeys.cloudName]: "my.shim.cloud.name" });
});
this._ai.trackEvent({ name: "Custom event" });
}
]
- .concat(this.asserts(1))
- .concat(PollingAssert.createPollingAssert(() => {
- const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
- if (payloadStr.length > 0) {
- Assert.equal(1, payloadStr.length, 'Only 1 track item is sent');
- const payload = JSON.parse(payloadStr[0]);
- Assert.ok(payload);
+ .concat(this.asserts(1))
+ .concat(PollingAssert.createPollingAssert(() => {
+ const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
+ if (payloadStr.length > 0) {
+ Assert.equal(1, payloadStr.length, 'Only 1 track item is sent');
+ const payload = JSON.parse(payloadStr[0]);
+ Assert.ok(payload);
- if (payload && payload.tags) {
- const tagResult: string = payload.tags && payload.tags[this.tagKeys.cloudName];
- const tagExpect: string = 'my.shim.cloud.name';
- Assert.equal(tagResult, tagExpect, 'telemetryinitializer tag override successful');
- return true;
+ if (payload && payload.tags) {
+ const tagResult: string = payload.tags && payload.tags[this.tagKeys.cloudName];
+ const tagExpect: string = 'my.shim.cloud.name';
+ Assert.equal(tagResult, tagExpect, 'telemetryinitializer tag override successful');
+ return true;
+ }
+ return false;
}
- return false;
- }
- }, 'Set custom tags') as any)
+ }, 'Set custom tags') as any)
});
this.testCaseAsync({
@@ -1737,41 +1791,41 @@ export class ApplicationInsightsTests extends AITestClass {
this._ai.addTelemetryInitializer((item: ITelemetryItem) => {
item.tags[this.tagKeys.cloudName] = "my.custom.cloud.name";
item.tags[this.tagKeys.locationCity] = "my.custom.location.city";
- item.tags.push({[this.tagKeys.locationCountry]: "my.custom.location.country"});
- item.tags.push({[this.tagKeys.operationId]: "my.custom.operation.id"});
+ item.tags.push({ [this.tagKeys.locationCountry]: "my.custom.location.country" });
+ item.tags.push({ [this.tagKeys.operationId]: "my.custom.operation.id" });
});
this._ai.trackEvent({ name: "Custom event via shimmed addTelemetryInitializer" });
}
]
- .concat(this.asserts(1))
- .concat(PollingAssert.createPollingAssert(() => {
- const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
- if (payloadStr.length > 0) {
- const payload = JSON.parse(payloadStr[0]);
- Assert.equal(1, payloadStr.length, 'Only 1 track item is sent - ' + payload.name);
- if (payloadStr.length > 1) {
- this.dumpPayloadMessages(this.successSpy);
- }
- Assert.ok(payload);
-
- if (payload && payload.tags) {
- const tagResult1: string = payload.tags && payload.tags[this.tagKeys.cloudName];
- const tagExpect1: string = 'my.custom.cloud.name';
- Assert.equal(tagResult1, tagExpect1, 'telemetryinitializer tag override successful');
- const tagResult2: string = payload.tags && payload.tags[this.tagKeys.locationCity];
- const tagExpect2: string = 'my.custom.location.city';
- Assert.equal(tagResult2, tagExpect2, 'telemetryinitializer tag override successful');
- const tagResult3: string = payload.tags && payload.tags[this.tagKeys.locationCountry];
- const tagExpect3: string = 'my.custom.location.country';
- Assert.equal(tagResult3, tagExpect3, 'telemetryinitializer tag override successful');
- const tagResult4: string = payload.tags && payload.tags[this.tagKeys.operationId];
- const tagExpect4: string = 'my.custom.operation.id';
- Assert.equal(tagResult4, tagExpect4, 'telemetryinitializer tag override successful');
- return true;
+ .concat(this.asserts(1))
+ .concat(PollingAssert.createPollingAssert(() => {
+ const payloadStr: string[] = this.getPayloadMessages(this.successSpy);
+ if (payloadStr.length > 0) {
+ const payload = JSON.parse(payloadStr[0]);
+ Assert.equal(1, payloadStr.length, 'Only 1 track item is sent - ' + payload.name);
+ if (payloadStr.length > 1) {
+ this.dumpPayloadMessages(this.successSpy);
+ }
+ Assert.ok(payload);
+
+ if (payload && payload.tags) {
+ const tagResult1: string = payload.tags && payload.tags[this.tagKeys.cloudName];
+ const tagExpect1: string = 'my.custom.cloud.name';
+ Assert.equal(tagResult1, tagExpect1, 'telemetryinitializer tag override successful');
+ const tagResult2: string = payload.tags && payload.tags[this.tagKeys.locationCity];
+ const tagExpect2: string = 'my.custom.location.city';
+ Assert.equal(tagResult2, tagExpect2, 'telemetryinitializer tag override successful');
+ const tagResult3: string = payload.tags && payload.tags[this.tagKeys.locationCountry];
+ const tagExpect3: string = 'my.custom.location.country';
+ Assert.equal(tagResult3, tagExpect3, 'telemetryinitializer tag override successful');
+ const tagResult4: string = payload.tags && payload.tags[this.tagKeys.operationId];
+ const tagExpect4: string = 'my.custom.operation.id';
+ Assert.equal(tagResult4, tagExpect4, 'telemetryinitializer tag override successful');
+ return true;
+ }
+ return false;
}
- return false;
- }
- }, 'Set custom tags') as any)
+ }, 'Set custom tags') as any)
});
this.testCaseAsync({
@@ -1779,7 +1833,7 @@ export class ApplicationInsightsTests extends AITestClass {
stepDelay: 1,
steps: [
() => {
- const context = (this._ai.context) as TelemetryContext;
+ const context = (this._ai.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10001');
this._ai.trackTrace({ message: 'authUserContext test' });
}
@@ -1789,7 +1843,7 @@ export class ApplicationInsightsTests extends AITestClass {
let payloadStr = this.getPayloadMessages(this.successSpy);
if (payloadStr.length > 0) {
let payloadEvents = payloadStr.length;
- let thePayload:string = payloadStr[0];
+ let thePayload: string = payloadStr[0];
if (payloadEvents !== 1) {
// Only 1 track should be sent
@@ -1810,7 +1864,7 @@ export class ApplicationInsightsTests extends AITestClass {
stepDelay: 1,
steps: [
() => {
- const context = (this._ai.context) as TelemetryContext;
+ const context = (this._ai.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10001', 'account123');
this._ai.trackTrace({ message: 'authUserContext test' });
}
@@ -1840,7 +1894,7 @@ export class ApplicationInsightsTests extends AITestClass {
stepDelay: 1,
steps: [
() => {
- const context = (this._ai.context) as TelemetryContext;
+ const context = (this._ai.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext("\u0428", "\u0429");
this._ai.trackTrace({ message: 'authUserContext test' });
}
@@ -1870,7 +1924,7 @@ export class ApplicationInsightsTests extends AITestClass {
stepDelay: 1,
steps: [
() => {
- const context = (this._ai.context) as TelemetryContext;
+ const context = (this._ai.context) as IPropTelemetryContext;
context.user.setAuthenticatedUserContext('10002', 'account567');
context.user.clearAuthenticatedUserContext();
this._ai.trackTrace({ message: 'authUserContext test' });
@@ -1901,7 +1955,7 @@ export class ApplicationInsightsTests extends AITestClass {
name: 'AuthenticatedUserContext: setAuthenticatedUserContext does not set the cookie by default',
test: () => {
// Setup
- const context = (this._ai.context) as TelemetryContext;
+ const context = (this._ai.context) as IPropTelemetryContext;
const authSpy: SinonSpy = this.sandbox.spy(context.user, 'setAuthenticatedUserContext');
let cookieMgr = this._ai.getCookieMgr();
const cookieSpy: SinonSpy = this.sandbox.spy(cookieMgr, 'set');
@@ -1930,7 +1984,7 @@ export class ApplicationInsightsTests extends AITestClass {
this.testCase({
name: 'iKey replacement: envelope will use the non-empty iKey defined in track method',
test: () => {
- this._ai.trackEvent({ name: 'event1', properties: { "prop1": "value1" }, measurements: { "measurement1": 200 }, iKey:"1a6933ad-aaaa-aaaa-aaaa-000000000000" });
+ this._ai.trackEvent({ name: 'event1', properties: { "prop1": "value1" }, measurements: { "measurement1": 200 }, iKey: "1a6933ad-aaaa-aaaa-aaaa-000000000000" });
Assert.ok(this.envelopeConstructorSpy.called);
const envelope = this.envelopeConstructorSpy.returnValues[0];
Assert.equal(envelope.iKey, "1a6933ad-aaaa-aaaa-aaaa-000000000000", "trackEvent iKey is replaced");
@@ -1940,7 +1994,7 @@ export class ApplicationInsightsTests extends AITestClass {
this.testCase({
name: 'iKey replacement: envelope will use the config iKey if defined ikey in track method is empty',
test: () => {
- this._ai.trackEvent({ name: 'event1', properties: { "prop1": "value1" }, measurements: { "measurement1": 200 }, iKey:"" });
+ this._ai.trackEvent({ name: 'event1', properties: { "prop1": "value1" }, measurements: { "measurement1": 200 }, iKey: "" });
Assert.ok(this.envelopeConstructorSpy.called);
const envelope = this.envelopeConstructorSpy.returnValues[0];
Assert.equal(envelope.iKey, ApplicationInsightsTests._instrumentationKey, "trackEvent iKey should not be replaced");
@@ -1959,7 +2013,7 @@ export class ApplicationInsightsTests extends AITestClass {
}
}
}
- private asserts: any = (expectedCount: number, includeInit:boolean = false, doBoilerPlate:boolean = true) => [
+ private asserts: any = (expectedCount: number, includeInit: boolean = false, doBoilerPlate: boolean = true) => [
() => {
const message = "polling: " + new Date().toISOString();
Assert.ok(true, message);
@@ -1989,21 +2043,22 @@ export class ApplicationInsightsTests extends AITestClass {
if (currentCount === expectedCount && !!this._ai.context.appId()) {
const payload = JSON.parse(payloadStr[0]);
const baseType = payload.data.baseType;
+
// call the appropriate Validate depending on the baseType
switch (baseType) {
- case Event.dataType:
+ case EventDataType:
return EventValidator.EventValidator.Validate(payload, baseType);
- case Trace.dataType:
+ case TraceDataType:
return TraceValidator.TraceValidator.Validate(payload, baseType);
- case Exception.dataType:
+ case ExceptionDataType:
return ExceptionValidator.ExceptionValidator.Validate(payload, baseType);
- case Metric.dataType:
+ case MetricDataType:
return MetricValidator.MetricValidator.Validate(payload, baseType);
- case PageView.dataType:
+ case PageViewDataType:
return PageViewValidator.PageViewValidator.Validate(payload, baseType);
- case PageViewPerformance.dataType:
+ case PageViewPerformanceDataType:
return PageViewPerformanceValidator.PageViewPerformanceValidator.Validate(payload, baseType);
- case RemoteDependencyData.dataType:
+ case RemoteDependencyDataType:
return RemoteDepdencyValidator.RemoteDepdencyValidator.Validate(payload, baseType);
default:
@@ -2020,9 +2075,9 @@ export class ApplicationInsightsTests extends AITestClass {
class CustomTestError extends Error {
constructor(message = "") {
- super(message);
- this.name = "CustomTestError";
- this.message = message + " -- test error.";
+ super(message);
+ this.name = "CustomTestError";
+ this.message = message + " -- test error.";
}
}
diff --git a/AISKU/Tests/Unit/src/sanitizer.e2e.tests.ts b/AISKU/Tests/Unit/src/sanitizer.e2e.tests.ts
index 3a6048f60..6e1d55b29 100644
--- a/AISKU/Tests/Unit/src/sanitizer.e2e.tests.ts
+++ b/AISKU/Tests/Unit/src/sanitizer.e2e.tests.ts
@@ -3,7 +3,7 @@ import { Sender } from '@microsoft/applicationinsights-channel-js';
import { AITestClass, Assert, PollingAssert } from '@microsoft/ai-test-framework';
import { SinonSpy } from 'sinon';
import { newId } from '@microsoft/applicationinsights-core-js';
-import { BreezeChannelIdentifier } from '@microsoft/applicationinsights-common';
+import { BreezeChannelIdentifier } from '@microsoft/applicationinsights-core-js';
export class SanitizerE2ETests extends AITestClass {
private readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
diff --git a/AISKU/Tests/Unit/src/sender.e2e.tests.ts b/AISKU/Tests/Unit/src/sender.e2e.tests.ts
index b74fa774f..9d22b6d2b 100644
--- a/AISKU/Tests/Unit/src/sender.e2e.tests.ts
+++ b/AISKU/Tests/Unit/src/sender.e2e.tests.ts
@@ -1,6 +1,6 @@
import { ApplicationInsights, IApplicationInsights } from '../../../src/applicationinsights-web'
import { Sender } from '@microsoft/applicationinsights-channel-js';
-import { BreezeChannelIdentifier, utlGetSessionStorage, utlRemoveSessionStorage } from '@microsoft/applicationinsights-common';
+import { BreezeChannelIdentifier, utlGetSessionStorage, utlRemoveSessionStorage } from '@microsoft/applicationinsights-core-js';
import { ActiveStatus, dumpObj, getJSON, isArray } from '@microsoft/applicationinsights-core-js';
import { SinonSpy } from 'sinon';
import { Assert, AITestClass, PollingAssert} from "@microsoft/ai-test-framework"
diff --git a/AISKU/Tests/Unit/src/validate.e2e.tests.ts b/AISKU/Tests/Unit/src/validate.e2e.tests.ts
index 95b8045a7..9adc6ddaa 100644
--- a/AISKU/Tests/Unit/src/validate.e2e.tests.ts
+++ b/AISKU/Tests/Unit/src/validate.e2e.tests.ts
@@ -3,7 +3,7 @@ import { Sender } from '@microsoft/applicationinsights-channel-js';
import { SinonSpy } from 'sinon';
import { AITestClass, Assert, PollingAssert } from '@microsoft/ai-test-framework';
import { dumpObj } from '@microsoft/applicationinsights-core-js';
-import { BreezeChannelIdentifier } from '@microsoft/applicationinsights-common';
+import { BreezeChannelIdentifier } from '@microsoft/applicationinsights-core-js';
export class ValidateE2ETests extends AITestClass {
private readonly _instrumentationKey = 'b7170927-2d1c-44f1-acec-59f4e1751c11';
diff --git a/AISKU/Tests/es6-module-type-check/package.json b/AISKU/Tests/es6-module-type-check/package.json
index 4e29d203e..b129e0fa7 100644
--- a/AISKU/Tests/es6-module-type-check/package.json
+++ b/AISKU/Tests/es6-module-type-check/package.json
@@ -32,7 +32,7 @@
"tslib": ">= 1.0.0"
},
"dependencies": {
- "@microsoft/applicationinsights-common": "3.3.11",
+ "@microsoft/applicationinsights-core-js": "3.3.11",
"@microsoft/applicationinsights-web": "3.3.11"
}
}
diff --git a/AISKU/Tests/es6-module-type-check/src/main.ts b/AISKU/Tests/es6-module-type-check/src/main.ts
index 9d6515a9c..227ed05a5 100644
--- a/AISKU/Tests/es6-module-type-check/src/main.ts
+++ b/AISKU/Tests/es6-module-type-check/src/main.ts
@@ -1,2 +1,2 @@
-import { ContextTagKeys } from "@microsoft/applicationinsights-common";
+import { ContextTagKeys } from "@microsoft/applicationinsights-core-js";
import { ITelemetryItem } from "@microsoft/applicationinsights-web";
\ No newline at end of file
diff --git a/AISKU/examples/span-usage-example.ts b/AISKU/examples/span-usage-example.ts
new file mode 100644
index 000000000..2df4a5d10
--- /dev/null
+++ b/AISKU/examples/span-usage-example.ts
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Example showing how to use the ApplicationInsights span functionality
+ * with the provider pattern.
+ */
+
+import { ApplicationInsights, eOTelSpanKind } from "@microsoft/applicationinsights-web";
+
+// Initialize ApplicationInsights
+const appInsights = new ApplicationInsights({
+ config: {
+ connectionString: "YOUR_CONNECTION_STRING_HERE"
+ }
+});
+appInsights.loadAppInsights();
+
+// Example usage
+function exampleSpanUsage() {
+ // Start a span
+ const span = appInsights.startSpan("example-operation", {
+ kind: eOTelSpanKind.CLIENT,
+ attributes: {
+ "operation.name": "example",
+ "user.id": "12345"
+ }
+ });
+
+ if (span) {
+ try {
+ // Do some work...
+ span.setAttribute("result", "success");
+ span.setAttribute("duration", 100);
+
+ // Create a child span
+ const childSpan = appInsights.startSpan("child-operation", {
+ kind: eOTelSpanKind.INTERNAL,
+ startTime: Date.now()
+ }, span.spanContext());
+
+ if (childSpan) {
+ // Do child work...
+ childSpan.setAttribute("child.data", "value");
+ childSpan.end();
+ }
+
+ } catch (error: any) {
+ span.setAttribute("error", true);
+ span.setAttribute("error.message", error.message);
+ } finally {
+ span.end();
+ }
+ }
+}
+
+// Example of checking if trace provider is available
+function checkTraceProviderAvailability() {
+ const provider = appInsights.getTraceProvider();
+ if (provider && provider.isAvailable()) {
+ console.log(`Trace provider available: ${provider.getProviderId()}`);
+ exampleSpanUsage();
+ } else {
+ console.log("No trace provider available");
+ }
+}
+
+export { exampleSpanUsage, checkTraceProviderAvailability };
diff --git a/AISKU/package.json b/AISKU/package.json
index 500bb7531..66a67338b 100644
--- a/AISKU/package.json
+++ b/AISKU/package.json
@@ -67,7 +67,6 @@
"@microsoft/applicationinsights-analytics-js": "3.3.11",
"@microsoft/applicationinsights-channel-js": "3.3.11",
"@microsoft/applicationinsights-cfgsync-js": "3.3.11",
- "@microsoft/applicationinsights-common": "3.3.11",
"@microsoft/applicationinsights-core-js": "3.3.11",
"@microsoft/applicationinsights-dependencies-js": "3.3.11",
"@microsoft/applicationinsights-properties-js": "3.3.11",
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/AISKU/src/AISku.ts b/AISKU/src/AISku.ts
index 5ca448464..51a55e209 100644
--- a/AISKU/src/AISku.ts
+++ b/AISKU/src/AISku.ts
@@ -7,19 +7,17 @@ import { AnalyticsPlugin, ApplicationInsights } from "@microsoft/applicationinsi
import { CfgSyncPlugin, ICfgSyncConfig, ICfgSyncMode } from "@microsoft/applicationinsights-cfgsync-js";
import { Sender } from "@microsoft/applicationinsights-channel-js";
import {
- AnalyticsPluginIdentifier, ConnectionString, DEFAULT_BREEZE_PATH, IAutoExceptionTelemetry, IConfig, IDependencyTelemetry,
- IEventTelemetry, IExceptionTelemetry, IMetricTelemetry, IPageViewPerformanceTelemetry, IPageViewTelemetry, IRequestHeaders,
- ITelemetryContext as Common_ITelemetryContext, IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, ITraceTelemetry,
- PropertiesPluginIdentifier, ThrottleMgr, parseConnectionString
-} from "@microsoft/applicationinsights-common";
-import {
- AppInsightsCore, FeatureOptInMode, IAppInsightsCore, IChannelControls, IConfigDefaults, IConfiguration, ICookieMgr, ICustomProperties,
- IDiagnosticLogger, IDistributedTraceContext, IDynamicConfigHandler, ILoadedPlugin, INotificationManager, IPlugin,
- ITelemetryInitializerHandler, ITelemetryItem, ITelemetryPlugin, ITelemetryUnloadState, IUnloadHook, UnloadHandler, WatcherFunction,
+ AnalyticsPluginIdentifier, AppInsightsCore, ConnectionString, DEFAULT_BREEZE_PATH, FeatureOptInMode, IAppInsightsCore,
+ IAutoExceptionTelemetry, IChannelControls, IConfig, IConfigDefaults, IConfiguration, ICookieMgr, ICustomProperties, IDependencyTelemetry,
+ IDiagnosticLogger, IDistributedTraceContext, IDynamicConfigHandler, IEventTelemetry, IExceptionTelemetry, ILoadedPlugin,
+ IMetricTelemetry, INotificationManager, IOTelApi, IOTelSpanOptions, IPageViewPerformanceTelemetry, IPageViewTelemetry, IPlugin,
+ IReadableSpan, IRequestHeaders, ISpanScope, ITelemetryContext as Common_ITelemetryContext, ITelemetryInitializerHandler, ITelemetryItem,
+ ITelemetryPlugin, ITelemetryUnloadState, IThrottleInterval, IThrottleLimit, IThrottleMgrConfig, ITraceApi, ITraceProvider,
+ ITraceTelemetry, IUnloadHook, OTelTimeInput, PropertiesPluginIdentifier, ThrottleMgr, UnloadHandler, WatcherFunction,
_eInternalMessageId, _throwInternal, addPageHideEventListener, addPageUnloadEventListener, cfgDfMerge, cfgDfValidate,
- createDynamicConfig, createProcessTelemetryContext, createUniqueNamespace, doPerf, eLoggingSeverity, hasDocument, hasWindow, isArray,
- isFeatureEnabled, isFunction, isNullOrUndefined, isReactNative, isString, mergeEvtNamespace, onConfigChange, proxyAssign, proxyFunctions,
- removePageHideEventListener, removePageUnloadEventListener
+ createDynamicConfig, createOTelApi, createProcessTelemetryContext, createTraceProvider, createUniqueNamespace, doPerf, eLoggingSeverity,
+ hasDocument, hasWindow, isArray, isFeatureEnabled, isFunction, isNullOrUndefined, isReactNative, isString, mergeEvtNamespace,
+ onConfigChange, parseConnectionString, proxyAssign, proxyFunctions, removePageHideEventListener, removePageUnloadEventListener, useSpan
} from "@microsoft/applicationinsights-core-js";
import {
AjaxPlugin as DependenciesPlugin, DependencyInitializerFunction, DependencyListenerFunction, IDependencyInitializerHandler,
@@ -27,24 +25,39 @@ import {
} from "@microsoft/applicationinsights-dependencies-js";
import { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js";
import { IPromise, createPromise, createSyncPromise, doAwaitResponse } from "@nevware21/ts-async";
-import { arrForEach, arrIndexOf, isPromiseLike, objDefine, objForEachKey, strIndexOf, throwUnsupported } from "@nevware21/ts-utils";
+import {
+ ICachedValue, arrForEach, arrIndexOf, dumpObj, getDeferred, isPromiseLike, objDefine, objForEachKey, strIndexOf, throwUnsupported
+} from "@nevware21/ts-utils";
import { IApplicationInsights } from "./IApplicationInsights";
import {
CONFIG_ENDPOINT_URL, STR_ADD_TELEMETRY_INITIALIZER, STR_CLEAR_AUTHENTICATED_USER_CONTEXT, STR_EVT_NAMESPACE, STR_GET_COOKIE_MGR,
STR_GET_PLUGIN, STR_POLL_INTERNAL_LOGS, STR_SET_AUTHENTICATED_USER_CONTEXT, STR_SNIPPET, STR_START_TRACK_EVENT, STR_START_TRACK_PAGE,
STR_STOP_TRACK_EVENT, STR_STOP_TRACK_PAGE, STR_TRACK_DEPENDENCY_DATA, STR_TRACK_EVENT, STR_TRACK_EXCEPTION, STR_TRACK_METRIC,
- STR_TRACK_PAGE_VIEW, STR_TRACK_TRACE
+ STR_TRACK_PAGE_VIEW, STR_TRACK_TRACE, UNDEFINED_VALUE
} from "./InternalConstants";
import { Snippet } from "./Snippet";
+import { createTelemetryItemFromSpan } from "./internal/trace/spanUtils";
export { IRequestHeaders };
let _internalSdkSrc: string;
+const STR_DEPENDENCIES = "dependencies";
+const STR_PROPERTIES = "properties";
+const STR_SNIPPET_VERSION = "_snippetVersion";
+const STR_APP_INSIGHTS_NEW = "appInsightsNew";
+const STR_GET_SKU_DEFAULTS = "getSKUDefaults";
+
// This is an exclude list of properties that should not be updated during initialization
// They include a combination of private and internal property names
const _ignoreUpdateSnippetProperties = [
- STR_SNIPPET, "dependencies", "properties", "_snippetVersion", "appInsightsNew", "getSKUDefaults"
+ STR_SNIPPET, STR_DEPENDENCIES, STR_PROPERTIES, STR_SNIPPET_VERSION, STR_APP_INSIGHTS_NEW, STR_GET_SKU_DEFAULTS, "trace", "otelApi"
+];
+
+// This is an exclude list of properties that should not be proxied to the snippet
+// They include a combination of private and internal property names
+const _ignoreProxyAssignProperties = [
+ STR_SNIPPET, STR_DEPENDENCIES, STR_PROPERTIES, STR_SNIPPET_VERSION, STR_APP_INSIGHTS_NEW, STR_GET_SKU_DEFAULTS
];
const IKEY_USAGE = "iKeyUsage";
@@ -52,8 +65,6 @@ const CDN_USAGE = "CdnUsage";
const SDK_LOADER_VER = "SdkLoaderVer";
const ZIP_PAYLOAD = "zipPayload";
-const UNDEFINED_VALUE: undefined = undefined;
-
const default_limit = {
samplingRate: 100,
maxSendNumber: 1
@@ -122,12 +133,25 @@ function _parseCs(config: IConfiguration & IConfig, configCs: string | IPromise<
});
}
+function _initOTel(sku: AppInsightsSku, traceName: string, onEnd: (span: IReadableSpan) => void, onException?: (span: IReadableSpan, exception: any, time?: OTelTimeInput) => void): ICachedValue {
+ let otelApi: ICachedValue = getDeferred(createOTelApi, [{
+ host: sku
+ }]);
+
+ // Create the initial default traceProvider
+ sku.core.setTraceProvider(getDeferred(() => {
+ return createTraceProvider(sku, traceName, otelApi.v, onEnd, onException);
+ }));
+
+ return otelApi;
+}
+
/**
* Application Insights API
* @group Entrypoint
* @group Classes
*/
-export class AppInsightsSku implements IApplicationInsights {
+export class AppInsightsSku implements IApplicationInsights {
public snippet: Snippet;
/**
@@ -151,6 +175,10 @@ export class AppInsightsSku implements IApplicationInsights {
*/
public readonly pluginVersionString: string;
+ public readonly trace: ITraceApi;
+
+ public readonly otelApi: IOTelApi;
+
constructor(snippet: Snippet) {
// NOTE!: DON'T set default values here, instead set them in the _initDefaults() function as it is also called during teardown()
let dependencies: DependenciesPlugin;
@@ -167,6 +195,7 @@ export class AppInsightsSku implements IApplicationInsights {
let _iKeySentMessage: boolean;
let _cdnSentMessage: boolean;
let _sdkVerSentMessage: boolean;
+ let _otelApi: ICachedValue;
dynamicProto(AppInsightsSku, this, (_self) => {
_initDefaults();
@@ -181,7 +210,7 @@ export class AppInsightsSku implements IApplicationInsights {
objDefine(_self, key, {
g: () => {
if (_core) {
- return _core[key];
+ return (_core as any)[key];
}
return null;
@@ -209,12 +238,25 @@ export class AppInsightsSku implements IApplicationInsights {
_sender = new Sender();
_core = new AppInsightsCore();
+
objDefine(_self, "core", {
g: () => {
return _core;
}
});
+ objDefine(_self, "otelApi", {
+ g: function() {
+ return _otelApi ? _otelApi.v : null;
+ }
+ });
+
+ objDefine(_self, "trace", {
+ g: function() {
+ return _otelApi ? _otelApi.v.trace : null;
+ }
+ });
+
// Will get recalled if any referenced values are changed
_addUnloadHook(onConfigChange(cfgHandler, () => {
let configCs = _config.connectionString;
@@ -314,8 +356,6 @@ export class AppInsightsSku implements IApplicationInsights {
}
});
};
-
-
_self.loadAppInsights = (legacyMode: boolean = false, logger?: IDiagnosticLogger, notificationManager?: INotificationManager): IApplicationInsights => {
if (legacyMode) {
@@ -338,8 +378,8 @@ export class AppInsightsSku implements IApplicationInsights {
!isFunction(value) &&
field && field[0] !== "_" && // Don't copy "internal" values
arrIndexOf(_ignoreUpdateSnippetProperties, field) === -1) {
- if (snippet[field] !== value) {
- snippet[field as string] = value;
+ if ((snippet as any)[field] !== value) {
+ (snippet as any)[field as string] = value;
}
}
});
@@ -349,9 +389,14 @@ export class AppInsightsSku implements IApplicationInsights {
doPerf(_self.core, () => "AISKU.loadAppInsights", () => {
// initialize core
_core.initialize(_config, [ _sender, properties, dependencies, _analyticsPlugin, _cfgSyncPlugin], logger, notificationManager);
+
+ // Initialize the initial OTel API
+ _otelApi = _initOTel(_self, "aisku", _onEnd, _onException);
+
objDefine(_self, "context", {
g: () => properties.context
});
+
if (!_throttleMgr){
_throttleMgr = new ThrottleMgr(_core);
}
@@ -402,7 +447,7 @@ export class AppInsightsSku implements IApplicationInsights {
// Note: This must be called before loadAppInsights is called
proxyAssign(snippet, _self, (name: string) => {
// Not excluding names prefixed with "_" as we need to proxy some functions like _onError
- return name && arrIndexOf(_ignoreUpdateSnippetProperties, name) === -1;
+ return name && arrIndexOf(_ignoreProxyAssignProperties, name) === -1;
});
};
@@ -528,7 +573,21 @@ export class AppInsightsSku implements IApplicationInsights {
if (!unloadDone) {
unloadDone = true;
+ // Reset OTel API to clean up all trace state before unloading core
+ if (_core) {
+ // Clear the trace provider to stop any active spans
+ _core.setTraceProvider(null);
+
+ // Reset the OTel API instances - this will be recreated on next init
+ if (_otelApi) {
+ _otelApi.v.shutdown();
+ }
+
+ _otelApi = null;
+ }
+
_initDefaults();
+
unloadComplete && unloadComplete(unloadState);
}
}
@@ -571,9 +630,16 @@ export class AppInsightsSku implements IApplicationInsights {
"addPlugin",
STR_EVT_NAMESPACE,
"addUnloadCb",
- "getTraceCtx",
"updateCfg",
- "onCfgChange"
+ "onCfgChange",
+ // ITraceHost Proxy
+ "getTraceCtx",
+ "setTraceCtx",
+ "startSpan",
+ "getActiveSpan",
+ "setActiveSpan",
+ "setTraceProvider",
+ "getTraceProvider"
]);
proxyFunctions(_self, () => {
@@ -583,7 +649,51 @@ export class AppInsightsSku implements IApplicationInsights {
STR_SET_AUTHENTICATED_USER_CONTEXT,
STR_CLEAR_AUTHENTICATED_USER_CONTEXT
]);
-
+
+ // Handle span end event - create telemetry from span data
+ function _onEnd(span: IReadableSpan) {
+ if (_otelApi && span && span.isRecording() && !_otelApi.v.cfg.traceCfg.suppressTracing) {
+
+ // Flip this span to be the "current" span during processing, so any telemetry created during the span processing
+ // is associated with this span
+ useSpan(_core, span, () => {
+ try {
+ // Create trace telemetry for the span
+ let telemetryItem: ITelemetryItem = createTelemetryItemFromSpan(_core, span);
+ if (telemetryItem) {
+ _self.core.track(telemetryItem);
+ }
+ } catch (error) {
+ // Log any errors during trace processing but don't let them break the span lifecycle
+ _throwInternal(_core.logger, eLoggingSeverity.WARNING,
+ _eInternalMessageId.TelemetryInitializerFailed,
+ "Error processing span - " + dumpObj(error));
+ }
+ });
+ }
+ }
+
+ function _onException(span: IReadableSpan, exception: any, time?: OTelTimeInput) {
+ if (_otelApi) {
+ // Flip this span to be the "current" span during processing, so any telemetry created during the span processing
+ useSpan(_core, span, () => {
+ try {
+ _self.trackException({
+ exception: exception,
+ properties: {
+ time: time
+ }
+ });
+ } catch (error) {
+ // Log any errors during exception processing but don't let them break the span lifecycle
+ _throwInternal(_core.logger, eLoggingSeverity.WARNING,
+ _eInternalMessageId.TelemetryInitializerFailed,
+ "Error processing exception - " + dumpObj(error));
+ }
+ });
+ }
+ }
+
// Using a function to support the dynamic adding / removal of plugins, so this will always return the current value
function _getCurrentDependencies() {
return dependencies;
@@ -790,7 +900,7 @@ export class AppInsightsSku implements IApplicationInsights {
/**
* Manually trigger an immediate send of all telemetry still in the buffer using beacon Sender.
* Fall back to xhr sender if beacon is not supported.
- * @param [async=true]
+ * @param async - send data asynchronously when true, default is true
*/
public onunloadFlush(async: boolean = true) {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
@@ -798,8 +908,8 @@ export class AppInsightsSku implements IApplicationInsights {
/**
* Initialize this instance of ApplicationInsights
- * @returns {IApplicationInsights}
* @param legacyMode - MUST always be false, it is no longer supported from v3.x onwards
+ * @returns The initialized {@link IApplicationInsights} instance
*/
public loadAppInsights(legacyMode: boolean = false, logger?: IDiagnosticLogger, notificationManager?: INotificationManager): IApplicationInsights {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
@@ -930,11 +1040,18 @@ export class AppInsightsSku implements IApplicationInsights {
/**
* Gets the current distributed trace context for this instance if available
*/
- public getTraceCtx(): IDistributedTraceContext | null | undefined {
+ public getTraceCtx(): IDistributedTraceContext | null {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
return null;
}
+ /**
+ * Sets the current distributed trace context for this instance if available
+ */
+ public setTraceCtx(newTraceCtx: IDistributedTraceContext | null | undefined): void {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ }
+
/**
* Watches and tracks changes for accesses to the current config, and if the accessed config changes the
* handler will be recalled.
@@ -945,6 +1062,72 @@ export class AppInsightsSku implements IApplicationInsights {
// @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
return null;
}
+
+ /**
+ * Start a new span with the given name and optional parent context.
+ *
+ * Note: This method only creates and returns the span. It does not automatically
+ * set the span as the active trace context. Context management should be handled
+ * separately using setTraceCtx() if needed.
+ *
+ * @param name - The name of the span
+ * @param options - Options for creating the span (kind, attributes, startTime)
+ * @param parent - Optional parent context. If not provided, uses the current active trace context
+ * @returns A new span instance, or null if no trace provider is available
+ * @since 3.4.0
+ */
+ public startSpan(name: string, options?: IOTelSpanOptions, parent?: IDistributedTraceContext): IReadableSpan | null {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ return null;
+ }
+
+ /**
+ * Return the current active span, if no trace provider is available null will be returned
+ * but when a trace provider is available a span instance will always be returned, even if
+ * there is no active span (in which case a non-recording span will be returned).
+ * @param createNew - Optional flag to create a non-recording span if no active span exists, defaults to true.
+ * When false, returns the existing active span or null without creating a non-recording span.
+ * @returns The current active span or null if no trace provider is available or if createNew is false and no active span exists
+ * @since 3.4.0
+ */
+ public getActiveSpan(createNew?: boolean): IReadableSpan | null {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ return null;
+ }
+
+ /**
+ * Set the current Active Span, if no trace provider is available the span will be not be set as the active span.
+ * @param span - The span to set as the active span
+ * @returns An ISpanScope instance that provides the current scope, the span will always be the span passed in
+ * even when no trace provider is available
+ * @since 3.4.0
+ */
+ public setActiveSpan(span: IReadableSpan): ISpanScope {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ return null;
+ }
+
+ /**
+ * Set the trace provider for creating spans.
+ * This allows different SKUs to provide their own span implementations.
+ *
+ * @param provider - The trace provider to use for span creation
+ * @since 3.4.0
+ */
+ public setTraceProvider(provider: ICachedValue): void {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ }
+
+ /**
+ * Get the current trace provider.
+ *
+ * @returns The current trace provider, or null if none is set
+ * @since 3.4.0
+ */
+ public getTraceProvider(): ITraceProvider | null {
+ // @DynamicProtoStub -- DO NOT add any code as this will be removed during packaging
+ return null;
+ }
}
// tslint:disable-next-line
diff --git a/AISKU/src/IApplicationInsights.ts b/AISKU/src/IApplicationInsights.ts
index 254d012b0..6d3cf4fc7 100644
--- a/AISKU/src/IApplicationInsights.ts
+++ b/AISKU/src/IApplicationInsights.ts
@@ -1,21 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-"use strict";
-
import { AnalyticsPlugin } from "@microsoft/applicationinsights-analytics-js";
import { Sender } from "@microsoft/applicationinsights-channel-js";
-import { IAppInsights, IPropertiesPlugin, IRequestHeaders } from "@microsoft/applicationinsights-common";
import {
- IConfiguration, ILoadedPlugin, IPlugin, ITelemetryPlugin, ITelemetryUnloadState, UnloadHandler
+ IAppInsights, IConfiguration, ILoadedPlugin, IOTelApi, IPlugin, IPropertiesPlugin, IRequestHeaders, ITelemetryPlugin,
+ ITelemetryUnloadState, ITraceApi, ITraceHost, UnloadHandler
} from "@microsoft/applicationinsights-core-js";
import { IDependenciesPlugin } from "@microsoft/applicationinsights-dependencies-js";
import { IPromise } from "@nevware21/ts-async";
export { IRequestHeaders };
-export interface IApplicationInsights extends IAppInsights, IDependenciesPlugin, IPropertiesPlugin {
+export interface IApplicationInsights extends IAppInsights, IDependenciesPlugin, IPropertiesPlugin, ITraceHost {
appInsights: AnalyticsPlugin;
+ /**
+ * The OpenTelemetry API instance associated with this instance
+ * Unlike OpenTelemetry, this API does not return a No-Op implementation and returns null if the SDK has been torn
+ * down or not yet initialized.
+ */
+ readonly otelApi: IOTelApi | null;
+
+ /**
+ * OpenTelemetry trace API for creating spans.
+ * Unlike OpenTelemetry, this API does not return a No-Op implementation and returns null if the SDK has been torn
+ * down or not yet initialized.
+ */
+ readonly trace: ITraceApi | null;
+
/**
* Attempt to flush data immediately; If executing asynchronously (the default) and
* you DO NOT pass a callback function then a [IPromise](https://nevware21.github.io/ts-async/typedoc/interfaces/IPromise.html)
@@ -59,7 +71,7 @@ export interface IApplicationInsights extends IAppInsights, IDependenciesPlugin,
/**
* Find and return the (first) plugin with the specified identifier if present
- * @param pluginIdentifier
+ * @param pluginIdentifier - The identifier of the plugin to find
*/
getPlugin(pluginIdentifier: string): ILoadedPlugin;
diff --git a/AISKU/src/Init.ts b/AISKU/src/Init.ts
index 22c39f5ab..a831b9a5e 100644
--- a/AISKU/src/Init.ts
+++ b/AISKU/src/Init.ts
@@ -44,7 +44,7 @@ export {
PropertiesPluginIdentifier,
BreezeChannelIdentifier,
AnalyticsPluginIdentifier
-} from "@microsoft/applicationinsights-common";
+} from "@microsoft/applicationinsights-core-js";
// ----------------------------------------------------------------------------------------------------
// End of Exports available from the Cdn bundles
@@ -82,11 +82,12 @@ export {
ITraceTelemetry,
IMetricTelemetry,
IEventTelemetry,
+ IRequestTelemetry,
IAppInsights,
eSeverityLevel,
IRequestHeaders,
EventPersistence
-} from "@microsoft/applicationinsights-common";
+} from "@microsoft/applicationinsights-core-js";
export { ISenderConfig } from "@microsoft/applicationinsights-channel-js";
diff --git a/AISKU/src/InternalConstants.ts b/AISKU/src/InternalConstants.ts
index 9e840b4d9..94c935053 100644
--- a/AISKU/src/InternalConstants.ts
+++ b/AISKU/src/InternalConstants.ts
@@ -10,6 +10,8 @@
const _AUTHENTICATED_USER_CONTEXT = "AuthenticatedUserContext";
const _TRACK = "track";
+
+export const UNDEFINED_VALUE: undefined = undefined;
export const STR_EMPTY = "";
export const STR_SNIPPET = "snippet";
export const STR_GET_COOKIE_MGR = "getCookieMgr";
diff --git a/AISKU/src/Snippet.ts b/AISKU/src/Snippet.ts
index 79955c805..45456a546 100644
--- a/AISKU/src/Snippet.ts
+++ b/AISKU/src/Snippet.ts
@@ -2,8 +2,7 @@
// Licensed under the MIT License.
"use strict";
-import { IConfig } from "@microsoft/applicationinsights-common";
-import { IConfiguration } from "@microsoft/applicationinsights-core-js";
+import { IConfig, IConfiguration } from "@microsoft/applicationinsights-core-js";
/**
*
diff --git a/AISKU/src/applicationinsights-web.ts b/AISKU/src/applicationinsights-web.ts
index 0ad8ca2b3..a627bf6a2 100644
--- a/AISKU/src/applicationinsights-web.ts
+++ b/AISKU/src/applicationinsights-web.ts
@@ -4,6 +4,9 @@ export { AppInsightsSku as ApplicationInsights } from "./AISku";
export { ApplicationInsightsContainer } from "./ApplicationInsightsContainer";
+// OpenTelemetry trace API exports (public interfaces only)
+export { IOTelTracerProvider, IOTelTracer, IAttributeContainer, IOTelAttributes, IReadableSpan } from "@microsoft/applicationinsights-core-js";
+
// Re-exports
export {
IConfiguration,
@@ -30,7 +33,11 @@ export {
INotificationManager,
IProcessTelemetryContext,
Tags,
- ILoadedPlugin
+ ILoadedPlugin,
+ IOTelSpan,
+ eOTelSpanKind,
+ OTelSpanKind,
+ IOTelSpanOptions
} from "@microsoft/applicationinsights-core-js";
export {
IConfig,
@@ -50,12 +57,11 @@ export {
Metric,
PageView,
PageViewPerformance,
- RemoteDependencyData,
Trace,
DistributedTracingModes,
IRequestHeaders,
EventPersistence
-} from "@microsoft/applicationinsights-common";
+} from "@microsoft/applicationinsights-core-js";
export { Sender, ISenderConfig } from "@microsoft/applicationinsights-channel-js";
export { ApplicationInsights as ApplicationAnalytics, IAppInsightsInternal, IAnalyticsConfig } from "@microsoft/applicationinsights-analytics-js";
export { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js";
diff --git a/AISKU/src/internal/trace/spanUtils.ts b/AISKU/src/internal/trace/spanUtils.ts
new file mode 100644
index 000000000..9ba03d029
--- /dev/null
+++ b/AISKU/src/internal/trace/spanUtils.ts
@@ -0,0 +1,506 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import {
+ ATTR_CLIENT_ADDRESS, ATTR_CLIENT_PORT, ATTR_ENDUSER_ID, ATTR_ENDUSER_PSEUDO_ID, ATTR_ERROR_TYPE, ATTR_EXCEPTION_MESSAGE,
+ ATTR_EXCEPTION_STACKTRACE, ATTR_EXCEPTION_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_ROUTE,
+ ATTR_NETWORK_LOCAL_ADDRESS, ATTR_NETWORK_LOCAL_PORT, ATTR_NETWORK_PEER_ADDRESS, ATTR_NETWORK_PEER_PORT, ATTR_NETWORK_PROTOCOL_NAME,
+ ATTR_NETWORK_PROTOCOL_VERSION, ATTR_NETWORK_TRANSPORT, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, ATTR_URL_PATH,
+ ATTR_URL_QUERY, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL, CtxTagKeys, DBSYSTEMVALUES_MONGODB, DBSYSTEMVALUES_MYSQL,
+ DBSYSTEMVALUES_POSTGRESQL, DBSYSTEMVALUES_REDIS, EXP_ATTR_ENDUSER_ID, EXP_ATTR_ENDUSER_PSEUDO_ID, EXP_ATTR_SYNTHETIC_TYPE,
+ IAppInsightsCore, IAttributeContainer, IConfiguration, IContextTagKeys, IDependencyTelemetry, IReadableSpan, IRequestTelemetry,
+ ITelemetryItem, MicrosoftClientIp, OTelAttributeValue, RemoteDependencyDataType, RemoteDependencyEnvelopeType, RequestDataType,
+ RequestEnvelopeType, SEMATTRS_DB_NAME, SEMATTRS_DB_OPERATION, SEMATTRS_DB_STATEMENT, SEMATTRS_DB_SYSTEM, SEMATTRS_ENDUSER_ID,
+ SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_EXCEPTION_STACKTRACE, SEMATTRS_EXCEPTION_TYPE, SEMATTRS_HTTP_CLIENT_IP, SEMATTRS_HTTP_FLAVOR,
+ SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_ROUTE, SEMATTRS_HTTP_SCHEME, SEMATTRS_HTTP_STATUS_CODE, SEMATTRS_HTTP_TARGET,
+ SEMATTRS_HTTP_URL, SEMATTRS_HTTP_USER_AGENT, SEMATTRS_NET_HOST_IP, SEMATTRS_NET_HOST_NAME, SEMATTRS_NET_HOST_PORT, SEMATTRS_NET_PEER_IP,
+ SEMATTRS_NET_PEER_NAME, SEMATTRS_NET_PEER_PORT, SEMATTRS_NET_TRANSPORT, SEMATTRS_PEER_SERVICE, SEMATTRS_RPC_GRPC_STATUS_CODE,
+ SEMATTRS_RPC_SYSTEM, Tags, createAttributeContainer, createTelemetryItem, eDependencyTypes, eOTelSpanKind, eOTelSpanStatusCode,
+ fieldRedaction, getDependencyTarget, getHttpMethod, getHttpStatusCode, getHttpUrl, getLocationIp, getUrl, getUserAgent,
+ hrTimeToMilliseconds, isSqlDB, isSyntheticSource, urlGetPathName
+} from "@microsoft/applicationinsights-core-js";
+import { ILazyValue, arrIncludes, asString, getLazy, isNullOrUndefined, strLower, strStartsWith } from "@nevware21/ts-utils";
+import { STR_EMPTY, UNDEFINED_VALUE } from "../../InternalConstants";
+
+/**
+ * Azure SDK namespace.
+ * @internal
+ */
+const AzNamespace = "az.namespace";
+const AzResourceNamespace = "azure.resource_provider.namespace";
+
+/**
+ * Azure SDK Eventhub.
+ * @internal
+ */
+const MicrosoftEventHub = "Microsoft.EventHub";
+
+/**
+ * Azure SDK message bus destination.
+ * @internal
+ */
+const MessageBusDestination = "message_bus.destination";
+
+/**
+ * AI time since enqueued attribute.
+ * @internal
+ */
+const TIME_SINCE_ENQUEUED = "timeSinceEnqueued";
+
+const PORT_REGEX: ILazyValue = (/*#__PURE__*/ getLazy(() => new RegExp(/(https?)(:\/\/.*)(:\d+)(\S*)/)));
+const HTTP_DOT = (/*#__PURE__*/ "http.");
+
+const _MS_PROCESSED_BY_METRICS_EXTRACTORS = (/* #__PURE__*/"_MS.ProcessedByMetricExtractors");
+
+/**
+ * Legacy HTTP semantic convention values
+ * @internal
+ */
+const _ignoreSemanticValues: ILazyValue = (/* #__PURE__*/ getLazy(_initIgnoreSemanticValues));
+
+/* #__NO_SIDE_EFFECTS__ */
+function _initIgnoreSemanticValues(): string[] {
+ return [
+ // Internal Microsoft attributes
+ _MS_PROCESSED_BY_METRICS_EXTRACTORS,
+
+ // Legacy HTTP semantic values
+ SEMATTRS_NET_PEER_IP,
+ SEMATTRS_NET_PEER_NAME,
+ SEMATTRS_NET_HOST_IP,
+ SEMATTRS_PEER_SERVICE,
+ SEMATTRS_HTTP_USER_AGENT,
+ SEMATTRS_HTTP_METHOD,
+ SEMATTRS_HTTP_URL,
+ SEMATTRS_HTTP_STATUS_CODE,
+ SEMATTRS_HTTP_ROUTE,
+ SEMATTRS_HTTP_HOST,
+ SEMATTRS_DB_SYSTEM,
+ SEMATTRS_DB_STATEMENT,
+ SEMATTRS_DB_OPERATION,
+ SEMATTRS_DB_NAME,
+ SEMATTRS_RPC_SYSTEM,
+ SEMATTRS_RPC_GRPC_STATUS_CODE,
+ SEMATTRS_EXCEPTION_TYPE,
+ SEMATTRS_EXCEPTION_MESSAGE,
+ SEMATTRS_EXCEPTION_STACKTRACE,
+ SEMATTRS_HTTP_SCHEME,
+ SEMATTRS_HTTP_TARGET,
+ SEMATTRS_HTTP_FLAVOR,
+ SEMATTRS_NET_TRANSPORT,
+ SEMATTRS_NET_HOST_NAME,
+ SEMATTRS_NET_HOST_PORT,
+ SEMATTRS_NET_PEER_PORT,
+ SEMATTRS_HTTP_CLIENT_IP,
+ SEMATTRS_ENDUSER_ID,
+ HTTP_DOT + "status_text",
+
+ // http Semabtic conventions
+ ATTR_CLIENT_ADDRESS,
+ ATTR_CLIENT_PORT,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+ ATTR_URL_FULL,
+ ATTR_URL_PATH,
+ ATTR_URL_QUERY,
+ ATTR_URL_SCHEME,
+ ATTR_ERROR_TYPE,
+ ATTR_NETWORK_LOCAL_ADDRESS,
+ ATTR_NETWORK_LOCAL_PORT,
+ ATTR_NETWORK_PROTOCOL_NAME,
+ ATTR_NETWORK_PEER_ADDRESS,
+ ATTR_NETWORK_PEER_PORT,
+ ATTR_NETWORK_PROTOCOL_VERSION,
+ ATTR_NETWORK_TRANSPORT,
+ ATTR_USER_AGENT_ORIGINAL,
+ ATTR_HTTP_REQUEST_METHOD,
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
+ ATTR_EXCEPTION_TYPE,
+ ATTR_EXCEPTION_MESSAGE,
+ ATTR_EXCEPTION_STACKTRACE,
+ EXP_ATTR_ENDUSER_ID,
+ EXP_ATTR_ENDUSER_PSEUDO_ID,
+ EXP_ATTR_SYNTHETIC_TYPE
+ ];
+}
+
+function _populateTagsFromSpan(telemetryItem: ITelemetryItem, span: IReadableSpan, contextKeys: IContextTagKeys, config: IConfiguration): void {
+
+ let tags: Tags = telemetryItem.tags = (telemetryItem.tags || [] as Tags);
+ let container = span.attribContainer || createAttributeContainer(span.attributes);
+
+ tags[contextKeys.operationId] = span.spanContext().traceId;
+ if ((span.parentSpanContext || {}).spanId) {
+ tags[contextKeys.operationParentId] = span.parentSpanContext.spanId;
+ }
+
+ // Map OpenTelemetry enduser attributes to Application Insights user attributes
+ const endUserId = container.get(ATTR_ENDUSER_ID);
+ if (endUserId) {
+ tags[contextKeys.userAuthUserId] = asString(endUserId);
+ }
+
+ const endUserPseudoId = container.get(ATTR_ENDUSER_PSEUDO_ID);
+ if (endUserPseudoId) {
+ tags[contextKeys.userId] = asString(endUserPseudoId);
+ }
+
+ const httpUserAgent = getUserAgent(container);
+ if (httpUserAgent) {
+ tags["ai.user.userAgent"] = String(httpUserAgent);
+ }
+ if (isSyntheticSource(container)) {
+ tags[contextKeys.operationSyntheticSource] = "True";
+ }
+
+ // Check for microsoft.client.ip first - this takes precedence over all other IP logic
+ const microsoftClientIp = container.get(MicrosoftClientIp);
+ if (microsoftClientIp) {
+ tags[contextKeys.locationIp] = asString(microsoftClientIp);
+ }
+
+ if (span.kind === eOTelSpanKind.SERVER) {
+ const httpMethod = getHttpMethod(container);
+ // Only use the fallback IP logic for server spans if microsoft.client.ip is not set
+ if (!microsoftClientIp) {
+ tags[contextKeys.locationIp] = getLocationIp(container);
+ }
+
+ if (httpMethod) {
+ const httpRoute = container.get(ATTR_HTTP_ROUTE);
+ const httpUrl = getHttpUrl(container);
+ tags[contextKeys.operationName] = span.name; // Default
+ if (httpRoute) {
+ // AiOperationName max length is 1024
+ // https://github.com/MohanGsk/ApplicationInsights-Home/blob/master/EndpointSpecs/Schemas/Bond/ContextTagKeys.bond
+ tags[contextKeys.operationName] = httpMethod + " " + fieldRedaction(asString(httpRoute), config);
+ } else if (httpUrl) {
+ try {
+ const urlPathName = fieldRedaction(urlGetPathName(asString(httpUrl)), config);
+ tags[contextKeys.operationName] = httpMethod + " " + urlPathName;
+ } catch {
+ /* no-op */
+ }
+ }
+ } else {
+ tags[contextKeys.operationName] = span.name;
+ }
+ } else {
+ let opName = container.get(contextKeys.operationName);
+ if (opName) {
+ tags[contextKeys.operationName] = opName as string;
+ }
+ }
+ // TODO: Location IP TBD for non server spans
+}
+
+/**
+ * Check to see if the key is in the list of known properties to ignore (exclude)
+ * from the properties collection
+ * @param key - the property key to check
+ * @param contextKeys - The current context keys
+ * @returns true if the key should be ignored, false otherwise
+ */
+function _isIgnorePropertiesKey(key: string, contextKeys: IContextTagKeys): boolean {
+ let result = false;
+
+ if (arrIncludes(_ignoreSemanticValues.v, key)) {
+ // The key is in set of known keys to ignore
+ result = true;
+ } else if (strStartsWith(key, "microsoft.")) {
+ // Ignoring all ALL keys starting with "microsoft."
+ result = true;
+ } else if (key === contextKeys.operationName) {
+ // Ignoring the key if it is the operation name context tag
+ result = true;
+ }
+
+ return result;
+}
+
+function _populatePropertiesFromAttributes(item: ITelemetryItem, contextKeys: IContextTagKeys, container: IAttributeContainer): void {
+ if (container) {
+ let baseData = item.baseData = (item.baseData || {});
+ let properties: { [propertyName: string]: any } = baseData.properties = (baseData.properties || {});
+
+ container.forEach((key: string, value) => {
+ // Avoid duplication ignoring fields already mapped.
+ if (!_isIgnorePropertiesKey(key, contextKeys)) {
+ properties[key] = value;
+ }
+ });
+ }
+}
+
+function _populateHttpDependencyProperties(dependencyTelemetry: IDependencyTelemetry, container: IAttributeContainer, httpMethod: OTelAttributeValue | undefined, config: IConfiguration): boolean {
+ if (httpMethod) {
+ // HTTP Dependency
+ const httpUrl = getHttpUrl(container);
+ if (httpUrl) {
+ try {
+ const pathName = urlGetPathName(asString(httpUrl));
+ if (pathName) {
+ dependencyTelemetry.name = httpMethod + " " + fieldRedaction(pathName, config);
+ }
+ } catch {
+ /* no-op */
+ }
+ }
+
+ dependencyTelemetry.type = eDependencyTypes.Http;
+ dependencyTelemetry.data = fieldRedaction(getUrl(container), config);
+ const httpStatusCode = getHttpStatusCode(container);
+ if (httpStatusCode) {
+ dependencyTelemetry.responseCode = +httpStatusCode;
+ }
+
+ let target = getDependencyTarget(container);
+ if (target) {
+ try {
+ // Remove default port
+ const res = PORT_REGEX.v.exec(target);
+ if (res !== null) {
+ const protocol = res[1];
+ const port = res[3];
+ if (
+ (protocol === "https" && port === ":443") ||
+ (protocol === "http" && port === ":80")
+ ) {
+ // Drop port
+ target = res[1] + res[2] + res[4];
+ }
+ }
+ } catch {
+ /* no-op */
+ }
+
+ dependencyTelemetry.target = target;
+ }
+ }
+
+ return !!httpMethod;
+}
+
+function _populateDbDependencyProperties(dependencyTelemetry: IDependencyTelemetry, container: IAttributeContainer, dbSystem: OTelAttributeValue | undefined): boolean {
+ if (dbSystem) {
+ // TODO: Remove special logic when Azure UX supports OpenTelemetry dbSystem
+ if (String(dbSystem) === DBSYSTEMVALUES_MYSQL) {
+ dependencyTelemetry.type = "mysql";
+ } else if (String(dbSystem) === DBSYSTEMVALUES_POSTGRESQL) {
+ dependencyTelemetry.type = "postgresql";
+ } else if (String(dbSystem) === DBSYSTEMVALUES_MONGODB) {
+ dependencyTelemetry.type = "mongodb";
+ } else if (String(dbSystem) === DBSYSTEMVALUES_REDIS) {
+ dependencyTelemetry.type = "redis";
+ } else if (isSqlDB(String(dbSystem))) {
+ dependencyTelemetry.type = "SQL";
+ } else {
+ dependencyTelemetry.type = String(dbSystem);
+ }
+ const dbStatement = container.get(SEMATTRS_DB_STATEMENT);
+ const dbOperation = container.get(SEMATTRS_DB_OPERATION);
+ if (dbStatement) {
+ dependencyTelemetry.data = String(dbStatement);
+ } else if (dbOperation) {
+ dependencyTelemetry.data = String(dbOperation);
+ }
+ const target = getDependencyTarget(container);
+ const dbName = container.get(SEMATTRS_DB_NAME);
+ if (target) {
+ dependencyTelemetry.target = dbName ? target + "|" + asString(dbName) : target;
+ } else {
+ dependencyTelemetry.target = dbName ? asString(dbName) : asString(dbSystem);
+ }
+ }
+
+ return !!dbSystem;
+}
+
+function _populateRpcDependencyProperties(dependencyTelemetry: IDependencyTelemetry, container: IAttributeContainer, rpcSystem: OTelAttributeValue | undefined): boolean {
+ if (rpcSystem) {
+ if (strLower(rpcSystem) === "wcf") {
+ dependencyTelemetry.type = eDependencyTypes.Wcf;
+ } else {
+ dependencyTelemetry.type = eDependencyTypes.Grpc;
+ }
+ const grpcStatusCode = container.get(SEMATTRS_RPC_GRPC_STATUS_CODE);
+ if (!isNullOrUndefined(grpcStatusCode)) {
+ dependencyTelemetry.responseCode = +grpcStatusCode;
+ }
+ const target = getDependencyTarget(container);
+ if (target) {
+ dependencyTelemetry.target = asString(target);
+ } else {
+ dependencyTelemetry.target = String(rpcSystem);
+ }
+ }
+
+ return !!rpcSystem;
+}
+
+function createDependencyTelemetryItem(core: IAppInsightsCore, span: IReadableSpan, contextKeys: IContextTagKeys): ITelemetryItem {
+ let container = span.attribContainer || createAttributeContainer(span.attributes);
+ let dependencyType = "Dependency";
+
+ if (span.kind === eOTelSpanKind.PRODUCER) {
+ dependencyType = eDependencyTypes.QueueMessage;
+ } else if (span.kind === eOTelSpanKind.INTERNAL && span.parentSpanContext) {
+ dependencyType = eDependencyTypes.InProc;
+ }
+
+ let spanCtx = span.spanContext();
+ let dependencyTelemetry: IDependencyTelemetry = {
+ name: span.name, // Default
+ id: spanCtx.spanId || core.getTraceCtx().spanId,
+ success: (span.status || {}).code !== eOTelSpanStatusCode.ERROR,
+ responseCode: 0,
+ type: dependencyType,
+ duration: hrTimeToMilliseconds(span.duration),
+ data: STR_EMPTY,
+ target: STR_EMPTY,
+ properties: UNDEFINED_VALUE,
+ measurements: UNDEFINED_VALUE
+ };
+
+ // Check for HTTP Dependency
+ if (!_populateHttpDependencyProperties(dependencyTelemetry, container, getHttpMethod(container), core.config)) {
+ // Check for DB Dependency
+ if (!_populateDbDependencyProperties(dependencyTelemetry, container, container.get(SEMATTRS_DB_SYSTEM))) {
+ // Check for Rpc Dependency
+ _populateRpcDependencyProperties(dependencyTelemetry, container, container.get(SEMATTRS_RPC_SYSTEM));
+ }
+ }
+
+ return createTelemetryItem(dependencyTelemetry, RemoteDependencyDataType, RemoteDependencyEnvelopeType.replace("{0}.", ""), core.logger);
+}
+
+function createRequestTelemetryItem(core: IAppInsightsCore, span: IReadableSpan, contextKeys: IContextTagKeys): ITelemetryItem {
+ let container = span.attribContainer || createAttributeContainer(span.attributes);
+
+ let spanCtx = span.spanContext();
+ const requestData: IRequestTelemetry = {
+ name: span.name, // Default
+ id: spanCtx.spanId || core.getTraceCtx().spanId,
+ success:
+ span.status.code !== eOTelSpanStatusCode.UNSET
+ ? span.status.code === eOTelSpanStatusCode.OK
+ : (Number(getHttpStatusCode(container)) || 0) < 400,
+ responseCode: 0,
+ duration: hrTimeToMilliseconds(span.duration),
+ source: undefined
+ };
+ const httpMethod = getHttpMethod(container);
+ const grpcStatusCode = container.get(SEMATTRS_RPC_GRPC_STATUS_CODE);
+ if (httpMethod) {
+ requestData.url = fieldRedaction(getUrl(container), core.config);
+ const httpStatusCode = getHttpStatusCode(container);
+ if (httpStatusCode) {
+ requestData.responseCode = +httpStatusCode;
+ }
+ } else if (grpcStatusCode) {
+ requestData.responseCode = +grpcStatusCode;
+ }
+
+ return createTelemetryItem(requestData, RequestDataType, RequestEnvelopeType.replace("{0}.", ""), core.logger);
+}
+
+export function createTelemetryItemFromSpan(core: IAppInsightsCore, span: IReadableSpan): ITelemetryItem | null {
+ let telemetryItem: ITelemetryItem = null;
+ let container = span.attribContainer || createAttributeContainer(span.attributes);
+ let contextKeys: IContextTagKeys = CtxTagKeys;
+ let kind = span.kind;
+ if (kind == eOTelSpanKind.SERVER || kind == eOTelSpanKind.CONSUMER) {
+ // Request
+ telemetryItem = createRequestTelemetryItem(core, span, contextKeys);
+ } else if (kind == eOTelSpanKind.CLIENT || kind == eOTelSpanKind.PRODUCER || kind == eOTelSpanKind.INTERNAL) {
+ // RemoteDependency
+ telemetryItem = createDependencyTelemetryItem(core, span, contextKeys);
+ } else {
+ //diag.error(`Unsupported span kind ${span.kind}`);
+ }
+
+ if (telemetryItem) {
+ // Set start time for the telemetry item from the event, not the time it is being processed (the default)
+ // The channel envelope creator uses this value when creating the envelope only when defined, otherwise it
+ // uses the time when the item is being processed
+ let baseData = telemetryItem.baseData = telemetryItem.baseData || {};
+ baseData.startTime = new Date(hrTimeToMilliseconds(span.startTime));
+
+ // Add dt extension to the telemetry item
+ let ext = telemetryItem.ext = telemetryItem.ext || {};
+ let dt = ext["dt"] = ext["dt"] || {};
+
+ // Don't overwrite any existing values
+ dt.spanId = dt.spanId || span.spanContext().spanId;
+ dt.traceId = dt.traceId || span.spanContext().traceId;
+
+ let traceFlags = span.spanContext().traceFlags;
+ if (!isNullOrUndefined(traceFlags)) {
+ dt.traceFlags = dt.traceFlags || traceFlags;
+ }
+
+ _populateTagsFromSpan(telemetryItem, span, contextKeys, core.config);
+ _populatePropertiesFromAttributes(telemetryItem, contextKeys, container);
+
+ let sampleRate = container.get("microsoft.sample_rate");
+ if (!isNullOrUndefined(sampleRate)) {
+ (telemetryItem as any).sampleRate = Number(sampleRate);
+ }
+
+ // Azure SDK
+ let azNamespace = container.get(AzResourceNamespace) || container.get(AzNamespace);
+ if (azNamespace) {
+ if (span.kind === eOTelSpanKind.INTERNAL) {
+ baseData.type = eDependencyTypes.InProc + " | " + azNamespace;
+ }
+
+ if (azNamespace === MicrosoftEventHub) {
+ _parseEventHubSpan(telemetryItem, span);
+ }
+ }
+ }
+
+ return telemetryItem;
+}
+
+/**
+ * Implementation of Mapping to Azure Monitor
+ *
+ * https://gist.github.com/lmolkova/e4215c0f44a49ef824983382762e6b92#mapping-to-azure-monitor-application-insights-telemetry
+ * @internal
+ */
+function _parseEventHubSpan(telemetryItem: ITelemetryItem, span: IReadableSpan): void {
+ let baseData = telemetryItem.baseData = telemetryItem.baseData || {};
+ let container = span.attribContainer || createAttributeContainer(span.attributes);
+ const namespace = container.get(AzResourceNamespace) || container.get(AzNamespace);
+ const peerAddress = asString(container.get(SEMATTRS_NET_PEER_NAME) || container.get("peer.address") || "unknown").replace(/\/$/g, ""); // remove trailing "/"
+ const messageBusDestination = (container.get(MessageBusDestination) || "unknown") as string;
+ let baseType = baseData.type || "";
+ let kind = span.kind;
+
+ if (kind === eOTelSpanKind.CLIENT) {
+ baseType = namespace;
+ baseData.target = peerAddress + "/" + messageBusDestination;
+ } else if (kind === eOTelSpanKind.PRODUCER) {
+ baseType = "Queue Message | " + namespace;
+ baseData.target = peerAddress + "/" + messageBusDestination;
+ } else if (kind === eOTelSpanKind.CONSUMER) {
+ baseType = "Queue Message | " + namespace;
+ (baseData as any).source = peerAddress + "/" + messageBusDestination;
+
+ let measurements = baseData.measurements = (baseData.measurements || {});
+ let timeSinceEnqueued = container.get("timeSinceEnqueued");
+ if (timeSinceEnqueued) {
+ measurements[TIME_SINCE_ENQUEUED] = Number(timeSinceEnqueued);
+ } else {
+ let enqueuedTime = parseFloat(asString(container.get("enqueuedTime")));
+ if (isNaN(enqueuedTime)) {
+ enqueuedTime = 0;
+ }
+
+ measurements[TIME_SINCE_ENQUEUED] = hrTimeToMilliseconds(span.startTime) - enqueuedTime;
+ }
+ }
+
+ baseData.type = baseType;
+}
diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts
index e4b7ba884..58cec6c38 100644
--- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts
+++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts
@@ -51,10 +51,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}
export class AISKULightSizeCheck extends AITestClass {
- private readonly MAX_RAW_SIZE = 94;
- private readonly MAX_BUNDLE_SIZE = 94;
- private readonly MAX_RAW_DEFLATE_SIZE = 39;
- private readonly MAX_BUNDLE_DEFLATE_SIZE = 39;
+ private readonly MAX_RAW_SIZE = 102;
+ private readonly MAX_BUNDLE_SIZE = 102;
+ private readonly MAX_RAW_DEFLATE_SIZE = 42;
+ private readonly MAX_BUNDLE_DEFLATE_SIZE = 42;
private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js";
private readonly currentVer = "3.3.11";
private readonly prodFilePath = `../browser/es5/aib.${this.currentVer[0]}.min.js`;
diff --git a/AISKULight/Tests/Unit/src/aiskuliteunittests.ts b/AISKULight/Tests/Unit/src/aiskuliteunittests.ts
index f1a132879..65eb86968 100644
--- a/AISKULight/Tests/Unit/src/aiskuliteunittests.ts
+++ b/AISKULight/Tests/Unit/src/aiskuliteunittests.ts
@@ -2,10 +2,12 @@ import { AISKULightSizeCheck } from "./AISKULightSize.Tests";
import { ApplicationInsightsDynamicConfigTests } from "./dynamicconfig.tests";
import { ApplicationInsightsConfigTests } from "./config.tests";
import { GlobalTestHooks } from "./GlobalTestHooks.Test";
+import { AISKULightOTelNegativeTests } from "./otelNegative.tests";
export function runTests() {
new GlobalTestHooks().registerTests();
new AISKULightSizeCheck().registerTests();
new ApplicationInsightsDynamicConfigTests().registerTests();
new ApplicationInsightsConfigTests().registerTests();
+ new AISKULightOTelNegativeTests().registerTests();
}
\ No newline at end of file
diff --git a/AISKULight/Tests/Unit/src/config.tests.ts b/AISKULight/Tests/Unit/src/config.tests.ts
index e234d8ff1..2cd6c46e5 100644
--- a/AISKULight/Tests/Unit/src/config.tests.ts
+++ b/AISKULight/Tests/Unit/src/config.tests.ts
@@ -1,7 +1,7 @@
import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework";
import { ITelemetryItem, newId } from "@microsoft/applicationinsights-core-js";
import { ApplicationInsights} from "../../../src/index";
-import { BreezeChannelIdentifier, ContextTagKeys, utlRemoveSessionStorage } from "@microsoft/applicationinsights-common";
+import { BreezeChannelIdentifier, ContextTagKeys, utlRemoveSessionStorage } from "@microsoft/applicationinsights-core-js";
import { Sender } from "@microsoft/applicationinsights-channel-js";
export class ApplicationInsightsConfigTests extends AITestClass {
diff --git a/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts b/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts
index 6bfc1e34b..1405c691c 100644
--- a/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts
+++ b/AISKULight/Tests/Unit/src/dynamicconfig.tests.ts
@@ -1,5 +1,5 @@
import { AITestClass, Assert, PollingAssert } from "@microsoft/ai-test-framework";
-import { IConfig } from "@microsoft/applicationinsights-common";
+import { IConfig } 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";
diff --git a/AISKULight/Tests/Unit/src/otelNegative.tests.ts b/AISKULight/Tests/Unit/src/otelNegative.tests.ts
new file mode 100644
index 000000000..f8fc5780e
--- /dev/null
+++ b/AISKULight/Tests/Unit/src/otelNegative.tests.ts
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { AITestClass, Assert } from "@microsoft/ai-test-framework";
+import { ApplicationInsights } from "../../../src/index";
+import { utlRemoveSessionStorage } from "@microsoft/applicationinsights-core-js";
+import { isNullOrUndefined, newId } from "@microsoft/applicationinsights-core-js";
+
+/**
+ * Negative tests for OpenTelemetry usage in AISKU Light
+ * These tests ensure that no exceptions are thrown and helpers behave correctly
+ * when there is no trace provider or OTel support instances
+ */
+export class AISKULightOTelNegativeTests extends AITestClass {
+ private readonly _instrumentationKey = "testIkey-1234-5678-9012-3456789012";
+ private _sessionPrefix: string;
+
+ public testInitialize() {
+ super.testInitialize();
+ this._sessionPrefix = newId();
+ }
+
+ public testCleanup() {
+ utlRemoveSessionStorage(null as any, "AI_sentBuffer");
+ utlRemoveSessionStorage(null as any, "AI_buffer");
+ utlRemoveSessionStorage(null as any, this._sessionPrefix + "_AI_sentBuffer");
+ utlRemoveSessionStorage(null as any, this._sessionPrefix + "_AI_buffer");
+ super.testCleanup();
+ }
+
+ public registerTests() {
+ this.addTraceContextWithoutProviderTests();
+ this.addUnloadWithoutProviderTests();
+ this.addConfigurationChangesWithoutProviderTests();
+ }
+
+ private addTraceContextWithoutProviderTests(): void {
+ this.testCase({
+ name: "AISKULight.getTraceCtx: should return valid context without trace provider",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix
+ };
+ const ai = new ApplicationInsights(config);
+ this.onDone(() => {
+ ai.unload(false);
+ });
+
+ // Act - no trace provider is set by default in AISKU Light
+ const ctx = ai.getTraceCtx();
+
+ // Assert - should return a valid context without throwing
+ Assert.ok(ctx !== undefined, "Should return a context (can be null)");
+
+ // If it returns a context, it should be valid
+ Assert.ok(!isNullOrUndefined(ctx?.traceId), "Context should have traceId");
+ Assert.ok(!isNullOrUndefined(ctx?.spanId), "Context should have spanId");
+ Assert.equal("", ctx?.spanId, "SpanId should be empty string without provider");
+ }
+ });
+
+ this.testCase({
+ name: "AISKULight.getTraceCtx: should not throw when called multiple times",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix
+ };
+ const ai = new ApplicationInsights(config);
+ this.onDone(() => {
+ ai.unload(false);
+ });
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _ctx1 = ai.getTraceCtx();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _ctx2 = ai.getTraceCtx();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _ctx3 = ai.getTraceCtx();
+
+ // Multiple calls should work without issues
+ Assert.ok(true, "Multiple getTraceCtx calls should not throw");
+ Assert.equal(_ctx1, _ctx2, "Multiple calls should return same context instance");
+ Assert.equal(_ctx2, _ctx3, "Multiple calls should return same context instance");
+ Assert.equal(_ctx1?.traceId, _ctx2?.traceId, "TraceId should be consistent across calls");
+ Assert.equal(_ctx2?.traceId, _ctx3?.traceId, "TraceId should be consistent across calls");
+ Assert.equal(_ctx1?.spanId, _ctx2?.spanId, "SpanId should be consistent across calls");
+ Assert.equal(_ctx2?.spanId, _ctx3?.spanId, "SpanId should be consistent across calls");
+ }, "Multiple getTraceCtx calls should be safe");
+ }
+ });
+
+ this.testCase({
+ name: "AISKULight: getTraceCtx should work after unload",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix
+ };
+ const ai = new ApplicationInsights(config);
+
+ // Act - unload first
+ ai.unload(false);
+
+ // Assert - should not throw even after unload
+ Assert.doesNotThrow(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _ctx = ai.getTraceCtx();
+ // Context might be null after unload, which is fine
+ }, "getTraceCtx should not throw after unload");
+ }
+ });
+ }
+
+
+
+ private addUnloadWithoutProviderTests(): void {
+ this.testCase({
+ name: "AISKULight: unload should work gracefully without trace provider",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix
+ };
+ const ai = new ApplicationInsights(config);
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ ai.unload(false);
+ }, "Unload should work without trace provider");
+
+ // Verify we can still access config after unload
+ Assert.ok(ai.config, "Config should still be accessible after unload");
+ }
+ });
+
+ this.testCase({
+ name: "AISKULight: unload with async flag should work without provider",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix
+ };
+ const ai = new ApplicationInsights(config);
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ ai.unload(true);
+ }, "Async unload should work without trace provider");
+ }
+ });
+ }
+
+ private addConfigurationChangesWithoutProviderTests(): void {
+ this.testCase({
+ name: "AISKULight: should handle traceCfg in config without trace provider",
+ test: () => {
+ // Arrange
+ const config = {
+ instrumentationKey: this._instrumentationKey,
+ namePrefix: this._sessionPrefix,
+ traceCfg: {
+ suppressTracing: false
+ }
+ };
+
+ // Act & Assert
+ Assert.doesNotThrow(() => {
+ const ai = new ApplicationInsights(config);
+
+ // Verify traceCfg is present
+ Assert.ok(ai.config.traceCfg, "traceCfg should be accessible");
+
+ this.onDone(() => {
+ ai.unload(false);
+ });
+ }, "Should handle traceCfg without trace provider");
+ }
+ });
+ }
+}
diff --git a/AISKULight/Tests/UnitTests.html b/AISKULight/Tests/UnitTests.html
index e5f022e3f..389753544 100644
--- a/AISKULight/Tests/UnitTests.html
+++ b/AISKULight/Tests/UnitTests.html
@@ -5,9 +5,9 @@
Tests for Application Insights JavaScript AISKU Light
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-