From ca58f1732ca5ab62e98806c40406261e7172b529 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Fri, 22 Aug 2025 10:36:29 -0700 Subject: [PATCH] Add globalJson property for global.json state to toplevelparser/command event --- src/Cli/dotnet/Program.cs | 13 +++++++++- src/Cli/dotnet/Telemetry/TelemetryFilter.cs | 24 +++++++++++++++---- .../Microsoft.DotNet.NativeWrapper/Interop.cs | 1 + .../SdkResolutionResult.cs | 8 +++++++ test/dotnet.Tests/TelemetryFilterTest.cs | 14 +++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index d88858b67e33..7762b77e8c09 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -230,7 +230,18 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) } PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart(); performanceData.Add("Startup Time", startupTime.TotalMilliseconds); - TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData)); + + string globalJsonState = string.Empty; + if (TelemetryClient.Enabled) + { + // Get the global.json state to report in telemetry along with this command invocation. + // We don't care about the actual SDK resolution, just the global.json information, + // so just pass empty string as executable directory for resolution. + NativeWrapper.SdkResolutionResult result = NativeWrapper.NETCoreSdkResolverNativeWrapper.ResolveSdk(string.Empty, Environment.CurrentDirectory); + globalJsonState = result.GlobalJsonState; + } + + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData, globalJsonState)); PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop(); int exitCode; diff --git a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs index 3c9b2cd2aaeb..753f7557e36e 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryFilter.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryFilter.cs @@ -26,24 +26,40 @@ public IEnumerable Filter(object objectToFilter) { var result = new List(); Dictionary measurements = null; + string globalJsonState = string.Empty; if (objectToFilter is Tuple> parseResultWithMeasurements) { objectToFilter = parseResultWithMeasurements.Item1; measurements = parseResultWithMeasurements.Item2; measurements = RemoveZeroTimes(measurements); } + else if (objectToFilter is Tuple, string> parseResultWithMeasurementsAndGlobalJsonState) + { + objectToFilter = parseResultWithMeasurementsAndGlobalJsonState.Item1; + measurements = parseResultWithMeasurementsAndGlobalJsonState.Item2; + measurements = RemoveZeroTimes(measurements); + globalJsonState = parseResultWithMeasurementsAndGlobalJsonState.Item3; + } if (objectToFilter is ParseResult parseResult) { var topLevelCommandName = parseResult.RootSubCommandResult(); if (topLevelCommandName != null) { + Dictionary properties = new() + { + ["verb"] = topLevelCommandName + }; + if (!string.IsNullOrEmpty(globalJsonState)) + { + properties["globalJson"] = globalJsonState; + } + result.Add(new ApplicationInsightsEntryFormat( "toplevelparser/command", - new Dictionary() - {{ "verb", topLevelCommandName }} - , measurements - )); + properties, + measurements + )); LogVerbosityForAllTopLevelCommand(result, parseResult, topLevelCommandName, measurements); diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs index 3ce65639989e..678577e9053f 100644 --- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs +++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Interop.cs @@ -80,6 +80,7 @@ internal enum hostfxr_resolve_sdk2_result_key_t : int resolved_sdk_dir = 0, global_json_path = 1, requested_version = 2, + global_json_state = 3, } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/SdkResolutionResult.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/SdkResolutionResult.cs index fac99ad2caa8..30f8cd00391b 100644 --- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/SdkResolutionResult.cs +++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/SdkResolutionResult.cs @@ -20,6 +20,11 @@ public class SdkResolutionResult /// public string? RequestedVersion; + /// + /// Result of the global.json search + /// + public string? GlobalJsonState; + /// /// True if a global.json was found but there was no compatible SDK, so it was ignored. /// @@ -38,6 +43,9 @@ internal void Initialize(Interop.hostfxr_resolve_sdk2_result_key_t key, string v case Interop.hostfxr_resolve_sdk2_result_key_t.requested_version: RequestedVersion = value; break; + case Interop.hostfxr_resolve_sdk2_result_key_t.global_json_state: + GlobalJsonState = value; + break; } } } diff --git a/test/dotnet.Tests/TelemetryFilterTest.cs b/test/dotnet.Tests/TelemetryFilterTest.cs index 115f5fb3086d..5b067a3f5cca 100644 --- a/test/dotnet.Tests/TelemetryFilterTest.cs +++ b/test/dotnet.Tests/TelemetryFilterTest.cs @@ -50,6 +50,20 @@ public void TopLevelCommandNameShouldBeSentToTelemetryWithPerformanceData() e.Measurement["Startup Time"] == 12345); } + [Fact] + public void TopLevelCommandNameShouldBeSentToTelemetryWithGlobalJsonState() + { + string globalJsonState = "invalid_data"; + var parseResult = Parser.Parse(["build"]); + TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, new Dictionary(), globalJsonState)); + _fakeTelemetry.LogEntries.Should().Contain(e => e.EventName == "toplevelparser/command" && + e.Properties.ContainsKey("verb") && + e.Properties["verb"] == Sha256Hasher.Hash("BUILD") && + e.Measurement == null && + e.Properties.ContainsKey("globalJson") && + e.Properties["globalJson"] == Sha256Hasher.HashWithNormalizedCasing(globalJsonState)); + } + [Fact] public void TopLevelCommandNameShouldBeSentToTelemetryWithZeroPerformanceData() {