Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e94fddc
Initial plan
Copilot Sep 29, 2025
01a7ffd
Add telemetry domain extraction feature with AppContext switch
Copilot Sep 29, 2025
8d87397
Complete telemetry domain extraction feature implementation with tests
Copilot Sep 29, 2025
4827171
internal method, no reflection in tests
westin-m Sep 30, 2025
e36a9b9
remove unnecessary operation
westin-m Sep 30, 2025
eee75f3
Fix AppContext switch test interference with ResetAppContextSwitches …
Copilot Sep 30, 2025
cb3550b
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
keegan-caruso Oct 6, 2025
621e684
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
keegan-caruso Oct 6, 2025
27828de
avoid tast failures causes by app context switch interference
westin-m Oct 7, 2025
53eab11
Merge branch 'copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3' of ht…
westin-m Oct 7, 2025
654c2c7
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
keegan-caruso Oct 7, 2025
1e001b6
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
westin-m Oct 7, 2025
6ee064d
Merge branch 'copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3' of ht…
westin-m Oct 7, 2025
6c190a8
pr feedback
westin-m Oct 8, 2025
ceeeb25
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
westin-m Oct 9, 2025
65ae428
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
westin-m Nov 4, 2025
86821d8
Merge branch 'dev' into copilot/fix-d17fdc27-51d2-412f-bc68-4eb999a2a2e3
keegan-caruso Nov 11, 2025
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
11 changes: 11 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/AppContextSwitches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ internal static class AppContextSwitches
private static bool? _useCapitalizedXMLTypeAttr;
internal static bool UseCapitalizedXMLTypeAttr => _useCapitalizedXMLTypeAttr ??= (AppContext.TryGetSwitch(UseCapitalizedXMLTypeAttrSwitch, out bool useCapitalizedXMLTypeAttr) && useCapitalizedXMLTypeAttr);

/// <summary>
/// When enabled, telemetry will use the full metadata address instead of just the domain name for IdentityModelConfiguration metrics.
/// By default (when disabled), only the domain name is used for successful operations to reduce OpenTelemetry cardinality.
/// </summary>
internal const string UseFullMetadataAddressForTelemetrySwitch = "Switch.Microsoft.IdentityModel.UseFullMetadataAddressForTelemetry";
private static bool? _useFullMetadataAddressForTelemetry;
internal static bool UseFullMetadataAddressForTelemetry => _useFullMetadataAddressForTelemetry ??= (AppContext.TryGetSwitch(UseFullMetadataAddressForTelemetrySwitch, out bool useFullMetadataAddressForTelemetry) && useFullMetadataAddressForTelemetry);

/// <summary>
/// Used for testing to reset all switches to its default value.
/// </summary>
Expand All @@ -123,6 +131,9 @@ internal static void ResetAllSwitches()

_useCapitalizedXMLTypeAttr = null;
AppContext.SetSwitch(UseCapitalizedXMLTypeAttrSwitch, false);

_useFullMetadataAddressForTelemetry = null;
AppContext.SetSwitch(UseFullMetadataAddressForTelemetrySwitch, false);
}
}
}
31 changes: 26 additions & 5 deletions src/Microsoft.IdentityModel.Tokens/Telemetry/TelemetryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,33 @@ internal class TelemetryClient : ITelemetryClient
AppContextSwitches.UpdateConfigAsBlocking.ToString()
);

/// <summary>
/// Extracts the domain name from a metadata address for telemetry purposes.
/// Returns the full address if domain extraction fails or if the UseFullMetadataAddressForTelemetry switch is enabled.
/// In error cases, always returns the full address for better debugging.
/// </summary>
/// <param name="metadataAddress">The full metadata address</param>
/// <param name="isSuccessCase">True if this is a successful operation, false for error cases</param>
/// <returns>Domain name for success cases (when switch is disabled), full address otherwise</returns>
internal static string GetMetadataAddressForTelemetry(string metadataAddress, bool isSuccessCase = true)
{
// Always use full address for error cases or when the switch is enabled
if (!isSuccessCase || AppContextSwitches.UseFullMetadataAddressForTelemetry || string.IsNullOrEmpty(metadataAddress))
return metadataAddress;


if (Uri.TryCreate(metadataAddress, UriKind.Absolute, out Uri result))
return result.Host;

return metadataAddress;
}

public void IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, string configurationSource)
{
var tagList = new TagList()
{
{ TelemetryConstants.IdentityModelVersionTag, ClientVer },
{ TelemetryConstants.MetadataAddressTag, metadataAddress },
{ TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: true) },
{ TelemetryConstants.OperationStatusTag, operationStatus },
{ TelemetryConstants.ConfigurationSourceTag, configurationSource },
_blockingTagValue
Expand All @@ -41,7 +62,7 @@ public void IncrementConfigurationRefreshRequestCounter(string metadataAddress,
var tagList = new TagList()
{
{ TelemetryConstants.IdentityModelVersionTag, ClientVer },
{ TelemetryConstants.MetadataAddressTag, metadataAddress },
{ TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) },
{ TelemetryConstants.OperationStatusTag, operationStatus },
{ TelemetryConstants.ConfigurationSourceTag, configurationSource },
{ TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() },
Expand All @@ -56,7 +77,7 @@ public void LogConfigurationRetrievalDuration(string metadataAddress, string con
var tagList = new TagList()
{
{ TelemetryConstants.IdentityModelVersionTag, ClientVer },
{ TelemetryConstants.MetadataAddressTag, metadataAddress },
{ TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: true) },
{ TelemetryConstants.ConfigurationSourceTag, configurationSource },
};

Expand All @@ -69,7 +90,7 @@ public void LogConfigurationRetrievalDuration(string metadataAddress, string con
var tagList = new TagList()
{
{ TelemetryConstants.IdentityModelVersionTag, ClientVer },
{ TelemetryConstants.MetadataAddressTag, metadataAddress },
{ TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) },
{ TelemetryConstants.ConfigurationSourceTag, configurationSource },
{ TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() },
_blockingTagValue
Expand All @@ -87,7 +108,7 @@ public void LogBackgroundConfigurationRefreshFailure(
var tagList = new TagList()
{
{ TelemetryConstants.IdentityModelVersionTag, ClientVer },
{ TelemetryConstants.MetadataAddressTag, metadataAddress },
{ TelemetryConstants.MetadataAddressTag, GetMetadataAddressForTelemetry(metadataAddress, isSuccessCase: false) },
{ TelemetryConstants.ConfigurationSourceTag, configurationSource },
{ TelemetryConstants.ExceptionTypeTag, exception.GetType().ToString() },
_blockingTagValue
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test kept failing when ran alongside the new ones introduced by this PR. Putting them in the same collection was the most minimal and only effective solution I found.

Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@

namespace Microsoft.IdentityModel.Tokens.Tests
{
[Collection(nameof(ClaimsIdentityFactoryTests))]
[ResetAppContextSwitches]
[Collection("AppContextSwitches")]
public class ClaimsIdentityFactoryTests
{
public ClaimsIdentityFactoryTests()
{
AppContextSwitches.ResetAllSwitches();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down Expand Up @@ -47,7 +43,7 @@ public void Create_FromTokenValidationParameters_ReturnsCorrectClaimsIdentity(bo
Assert.Equal(jsonWebToken, ((CaseSensitiveClaimsIdentity)actualClaimsIdentity).SecurityToken);
}

AppContextSwitches.ResetAllSwitches();
AppContext.SetSwitch(AppContextSwitches.UseClaimsIdentityTypeSwitch, false);
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
using Xunit;

namespace Microsoft.IdentityModel.Telemetry.Tests;

[ResetAppContextSwitches]
[Collection("AppContextSwitches")]
public class TelemetryClientDomainExtractionTests
{
[Theory]
[InlineData("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "login.microsoftonline.com")]
[InlineData("https://www.login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "www.login.microsoftonline.com")]
[InlineData("https://accounts.google.com/.well-known/openid-configuration", "accounts.google.com")]
[InlineData("https://login.windows.net/common/.well-known/openid-configuration", "login.windows.net")]
[InlineData("http://localhost:8080/.well-known/openid-configuration", "localhost")]
[InlineData("https://example.com/path/to/config", "example.com")]
[InlineData("https://subdomain.example.org/config.json", "subdomain.example.org")]
public void GetMetadataAddressForTelemetry_SuccessCase_ReturnsExpectedDomain(string fullAddress, string expectedDomain)
{
// Act
var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, true);

// Assert
Assert.Equal(expectedDomain, result);
}

[Fact]
public void DebugAppContextSwitch()
{
// Check if the AppContext switch is causing issues
var switchValue = AppContextSwitches.UseFullMetadataAddressForTelemetry;

var testUrl = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
var result = TelemetryClient.GetMetadataAddressForTelemetry(testUrl);

// The switch should be false by default, so we should get the host name
Assert.False(switchValue, $"Switch value: {switchValue}");
Assert.Equal("login.microsoftonline.com", result);
}

[Theory]
[InlineData("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration")]
[InlineData("https://accounts.google.com/.well-known/openid-configuration")]
[InlineData("https://login.windows.net/common/.well-known/openid-configuration")]
public void GetMetadataAddressForTelemetry_ErrorCase_ReturnsFullAddress(string fullAddress)
{
// Act
var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, false);

// Assert
Assert.Equal(fullAddress, result);
}

[Theory]
[InlineData("invalid-url")]
[InlineData("")]
[InlineData(null)]
public void GetMetadataAddressForTelemetry_InvalidUrl_ReturnsOriginalString(string invalidUrl)
{
// Act
var result = TelemetryClient.GetMetadataAddressForTelemetry(invalidUrl, true);

// Assert
Assert.Equal(invalidUrl, result);
}

[Fact]
public void GetMetadataAddressForTelemetry_WithAppContextSwitch_ReturnsFullAddress()
{
// Arrange
var fullAddress = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";

try
{
// Enable the switch to use full metadata address
AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, true);

// Act
var result = TelemetryClient.GetMetadataAddressForTelemetry(fullAddress, true);

// Assert
Assert.Equal(fullAddress, result);
}
finally
{
// Cleanup is handled by ResetAppContextSwitches attribute
AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, false);
}
}

[Fact]
public void TelemetryClient_Methods_DoNotThrow()
{
// This test ensures that the modified telemetry methods don't break existing functionality
// We cannot easily verify the exact values sent to the telemetry system without complex setup,
// but we can ensure the methods don't throw exceptions when called with valid parameters.

// Arrange
var telemetryClient = new TelemetryClient();
var testAddress = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
var testDuration = TimeSpan.FromMilliseconds(100);
var testException = new InvalidOperationException("Test exception");

// Act & Assert - these should not throw
var ex1 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration));
var ex2 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration, testException));
var ex3 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "FirstRefresh", "Retriever"));
var ex4 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "ConfigurationRetrievalFailed", "Retriever", testException));
var ex5 = Record.Exception(() => telemetryClient.LogBackgroundConfigurationRefreshFailure(testAddress, "Retriever", testException));

Assert.Null(ex1);
Assert.Null(ex2);
Assert.Null(ex3);
Assert.Null(ex4);
Assert.Null(ex5);
}

[Fact]
public void TelemetryClient_WithAppContextSwitch_DoesNotThrow()
{
// Test that telemetry methods work correctly when the backward compatibility switch is enabled
try
{
// Enable the switch to use full metadata address
AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, true);

// Arrange
var telemetryClient = new TelemetryClient();
var testAddress = "https://accounts.google.com/.well-known/openid-configuration";
var testDuration = TimeSpan.FromMilliseconds(150);
var testException = new InvalidOperationException("Test exception");

// Act & Assert - these should not throw even with the switch enabled
var ex1 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration));
var ex2 = Record.Exception(() => telemetryClient.LogConfigurationRetrievalDuration(testAddress, "Retriever", testDuration, testException));
var ex3 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "FirstRefresh", "Retriever"));
var ex4 = Record.Exception(() => telemetryClient.IncrementConfigurationRefreshRequestCounter(testAddress, "ConfigurationRetrievalFailed", "Retriever", testException));
var ex5 = Record.Exception(() => telemetryClient.LogBackgroundConfigurationRefreshFailure(testAddress, "Retriever", testException));

Assert.Null(ex1);
Assert.Null(ex2);
Assert.Null(ex3);
Assert.Null(ex4);
Assert.Null(ex5);
}
finally
{
// Cleanup
AppContext.SetSwitch(AppContextSwitches.UseFullMetadataAddressForTelemetrySwitch, false);
}
}
}
Loading