Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .aiAutoMinify.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"constEnums": [
"_eSetDynamicPropertyFlags",
"CallbackType",
"eTraceStateKeyType",
"eEventsDiscardedReason",
"eBatchDiscardedReason",
"FeatureOptInMode",
Expand All @@ -15,7 +16,9 @@
"TransportType",
"eStatsType",
"TelemetryUnloadReason",
"TelemetryUpdateReason"
"TelemetryUpdateReason",
"eTraceHeadersMode",
"eW3CTraceFlags"
]
},
"@microsoft/applicationinsights-perfmarkmeasure-js": {
Expand Down
119 changes: 111 additions & 8 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions AISKU/Tests/Unit/src/AISKUSize.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}

export class AISKUSizeCheck extends AITestClass {
private readonly MAX_RAW_SIZE = 148;
private readonly MAX_BUNDLE_SIZE = 148;
private readonly MAX_RAW_DEFLATE_SIZE = 59;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 59;
private readonly MAX_RAW_SIZE = 154;
private readonly MAX_BUNDLE_SIZE = 154;
private readonly MAX_RAW_DEFLATE_SIZE = 62;
private readonly MAX_BUNDLE_DEFLATE_SIZE = 62;
private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js";
// Automatically updated by version scripts
private readonly currentVer = "3.3.9";
Expand Down
13 changes: 7 additions & 6 deletions AISKU/Tests/Unit/src/SnippetInitialization.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
utlRemoveSessionStorage, utlSetSessionStorage
} from "@microsoft/applicationinsights-common";
import { getGlobal } from "@microsoft/applicationinsights-shims";
import { TelemetryContext } from "@microsoft/applicationinsights-properties-js";
import { IPropTelemetryContext } from "@microsoft/applicationinsights-properties-js";
import { dumpObj, objHasOwnProperty, strSubstring } from "@nevware21/ts-utils";
import { AppInsightsSku } from "../../../src/AISku";

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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' });
}
Expand Down Expand Up @@ -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' });
}
Expand Down Expand Up @@ -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' });
}
Expand Down Expand Up @@ -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' });
Expand Down Expand Up @@ -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');
Expand Down
13 changes: 7 additions & 6 deletions AISKU/Tests/Unit/src/applicationinsights.e2e.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 { 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';
Expand Down Expand Up @@ -1660,6 +1660,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);
Expand Down Expand Up @@ -1788,7 +1789,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' });
}
Expand Down Expand Up @@ -1819,7 +1820,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' });
}
Expand Down Expand Up @@ -1849,7 +1850,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' });
}
Expand Down Expand Up @@ -1879,7 +1880,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' });
Expand Down Expand Up @@ -1910,7 +1911,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');
Expand Down
2 changes: 1 addition & 1 deletion AISKU/src/AISku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,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
Expand Down
8 changes: 4 additions & 4 deletions AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly:
}

export class AISKULightSizeCheck extends AITestClass {
private readonly MAX_RAW_SIZE = 93;
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 = 99;
private readonly MAX_BUNDLE_SIZE = 99;
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.9";
private readonly prodFilePath = `../browser/es5/aib.${this.currentVer[0]}.min.js`;
Expand Down
Loading