From cdeb828c557268f2994599bfb5b11f7284949eb3 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:01:51 -0700 Subject: [PATCH 1/3] Fix project context event delivery classification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/telemetry.md | 2 +- .../ProjectContextCommandTelemetryTests.cs | 4 +- .../WinApp.Cli.Tests/TelemetryTests.cs | 51 +++++++++++++++++++ .../Telemetry/Events/ProjectContextEvent.cs | 4 +- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/telemetry.md b/docs/telemetry.md index 7f6ede06a..0f2a241de 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -88,7 +88,7 @@ The telemetry feature collects the following data: | CLI version | The version of the winapp CLI tool (from assembly version). | | CI environment | A boolean flag indicating whether the CLI is running in a Continuous Integration environment. | | Caller | The value of the `WINAPP_CLI_CALLER` environment variable, if set. This allows wrapper tools (like the npm package) to identify themselves. | -| Project context | For `init`, `run`, `restore`, `update`, and `package`, a separate correlated event records bounded categories describing the project family (`dotnet`, `node`, `cpp`, `rust`, `dart`, `hybrid`, `mixed`, or `unknown`), recognized app framework (such as `winui`, `wpf`, `winforms`, `maui`, `electron`, `tauri`, `flutter`, `react-native-windows`, `avalonia`, `uwp`, `windows-app-sdk`, or `other-dotnet`), target kind, detection source, confidence, packaging model, and whether `run` used project or folder execution. It doesn't include project names, paths, repositories, versions, dependency lists, or source. | +| Project context | For `init`, `run`, `restore`, `update`, and `package`, a separate correlated event records bounded categories describing the project family (`dotnet`, `node`, `cpp`, `rust`, `dart`, `hybrid`, `mixed`, or `unknown`), recognized app framework (such as `winui`, `wpf`, `winforms`, `maui`, `electron`, `tauri`, `flutter`, `react-native-windows`, `avalonia`, `uwp`, `windows-app-sdk`, or `other-dotnet`), target kind, detection source, confidence, packaging model, and whether `run` used project or folder execution. This event is collected at full rate when telemetry is enabled. It doesn't include project names, paths, repositories, versions, dependency lists, or source. | | `find-ui` usage | For the `winapp find-ui` command only, an additional usage event with non-personal, bounded values: the mode (`search`, `fetch`, or `list`); the selected `--source` (a fixed value — `gallery`, `toolkit`, `reactor`, or `core`); the catalog scenario IDs fetched (e.g. `gallery-tabview-1`), which identify built-in WinUI sample controls, never your code; whether `--json` was used; and result/ID counts. The free-form search query is **never** collected, and any requested IDs that don't match a real catalog entry are counted but **not** collected as text. | | `find-api` usage | For the `winapp find-api` command only, an additional usage event with non-personal, bounded values: the verb (a fixed value — `search`, `members`, `check-property`, `types`, `enums`, `namespaces`, `packages`, `stats`, `projects`, or `refresh`); whether `--json` was used; a result count; and whether the lookup found a match. The free-form arguments — the search query and any type, namespace, or property name — are **never** collected. | | `winapp ui` desktop coordination | For locally executed `winapp ui` commands only, a privacy-minimized summary of how the command shared the desktop with other UI workflows: how the workflow identity was resolved (`Workflow` or `Anonymous` — never the identity itself), the coordination mode (`Observe`, `TurnShared`, or `DesktopExclusive`), how the turn was obtained (new, continuation, queued, handoff-after-idle, or detached), the outcome (completed, cancelled, coordination failure, or corruption recovery), and **coarse buckets** for how long this command waited for the desktop, how many commands were queued, and how long the owning workflow had held its turn. | diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs index 2eb33db8e..b2b3a3e85 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs @@ -97,14 +97,14 @@ public async Task Init_ExplicitNestedDirectory_PreservesAncestorConfidence() } [TestMethod] - public void EnabledTelemetry_EmitsProjectContextAtMeasureLevel() + public void EnabledTelemetry_EmitsProjectContextAtCriticalLevel() { ProjectContextEvent.Log( "restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); Assert.AreEqual("ProjectContext_Event", _telemetry.EventNames.Single()); - Assert.AreEqual(LogLevel.Measure, _telemetry.Levels.Single()); + Assert.AreEqual(LogLevel.Critical, _telemetry.Levels.Single()); } [TestMethod] diff --git a/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs index bd787e787..4a5d6efd6 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs @@ -8,6 +8,7 @@ using Microsoft.Diagnostics.Telemetry.Internal; using WinApp.Cli.Commands; using WinApp.Cli.Helpers; +using WinApp.Cli.Services; using WinApp.Cli.Telemetry; using WinApp.Cli.Telemetry.Events; @@ -89,6 +90,56 @@ public void CommandEvents_EmitTargetKindInBothPayloads() "ExecutionTargetKind", "sandbox"); } + [TestMethod] + public void ProjectContextEvent_UsesCriticalKeywordWithoutEnablingDiagnostics() + { + using var listener = new CapturingEventListener(ProviderName); + var telemetry = new WinApp.Cli.Telemetry.Telemetry(); + TelemetryFactory.SetOverrideForTesting(telemetry); + try + { + ProjectContextEvent.Log("restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); + ProjectContextEvent.Log("run", () => ProjectContext.Unknown(ProjectTargetKind.BuildOutput)); + telemetry.Log("OtherMeasure", LogLevel.Measure, new ProbeEvent { Detail = "local-only" }); + + var events = listener.WaitForEvents(3); + Assert.IsFalse(telemetry.IsDiagnosticTelemetryOn); + AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, + "Command", "restore"); + AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, + "Command", "run"); + AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, + "ProjectFamily", "unknown"); + AssertEvent(events, "OtherMeasure", EventLevel.Verbose, EventKeywords.None, + "Detail", "local-only"); + } + finally + { + TelemetryFactory.SetOverrideForTesting(null); + } + } + + [TestMethod] + public void ProjectContextEvent_OptOutSkipsClassificationAndEmission() + { + Environment.SetEnvironmentVariable("WINAPP_CLI_TELEMETRY_OPTOUT", "1"); + using var listener = new CapturingEventListener(ProviderName); + var telemetry = new WinApp.Cli.Telemetry.Telemetry(); + TelemetryFactory.SetOverrideForTesting(telemetry); + try + { + ProjectContextEvent.Log("restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); + ProjectContextEvent.Log("run", () => throw new AssertFailedException("Opt-out must not inspect project metadata.")); + + Assert.IsFalse(telemetry.IsTelemetryOn); + Assert.IsFalse(listener.WaitForEvents(0).Any(e => e.Name == "ProjectContext_Event")); + } + finally + { + TelemetryFactory.SetOverrideForTesting(null); + } + } + [TestMethod] public void AddSensitiveString_IgnoresUnsafeEntriesAndSanitizesEventBaseFields() { diff --git a/src/winapp-CLI/WinApp.Cli/Telemetry/Events/ProjectContextEvent.cs b/src/winapp-CLI/WinApp.Cli/Telemetry/Events/ProjectContextEvent.cs index f09fa57ed..279a9a0d3 100644 --- a/src/winapp-CLI/WinApp.Cli/Telemetry/Events/ProjectContextEvent.cs +++ b/src/winapp-CLI/WinApp.Cli/Telemetry/Events/ProjectContextEvent.cs @@ -68,7 +68,7 @@ public static void Log(string command, ProjectContext context) telemetry.Log( "ProjectContext_Event", - LogLevel.Measure, + LogLevel.Critical, new ProjectContextEvent(command, context), TelemetryCorrelation.CurrentId); } @@ -83,7 +83,7 @@ public static void Log(string command, Func createContext) telemetry.Log( "ProjectContext_Event", - LogLevel.Measure, + LogLevel.Critical, new ProjectContextEvent(command, createContext()), TelemetryCorrelation.CurrentId); } From cfd3cc7d12d07877514af4e6dc8e6739b0b438f8 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:38:22 -0700 Subject: [PATCH 2/3] Trim project context fix to production only Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/telemetry.md | 2 +- .../ProjectContextCommandTelemetryTests.cs | 4 +- .../WinApp.Cli.Tests/TelemetryTests.cs | 51 ------------------- 3 files changed, 3 insertions(+), 54 deletions(-) diff --git a/docs/telemetry.md b/docs/telemetry.md index 0f2a241de..7f6ede06a 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -88,7 +88,7 @@ The telemetry feature collects the following data: | CLI version | The version of the winapp CLI tool (from assembly version). | | CI environment | A boolean flag indicating whether the CLI is running in a Continuous Integration environment. | | Caller | The value of the `WINAPP_CLI_CALLER` environment variable, if set. This allows wrapper tools (like the npm package) to identify themselves. | -| Project context | For `init`, `run`, `restore`, `update`, and `package`, a separate correlated event records bounded categories describing the project family (`dotnet`, `node`, `cpp`, `rust`, `dart`, `hybrid`, `mixed`, or `unknown`), recognized app framework (such as `winui`, `wpf`, `winforms`, `maui`, `electron`, `tauri`, `flutter`, `react-native-windows`, `avalonia`, `uwp`, `windows-app-sdk`, or `other-dotnet`), target kind, detection source, confidence, packaging model, and whether `run` used project or folder execution. This event is collected at full rate when telemetry is enabled. It doesn't include project names, paths, repositories, versions, dependency lists, or source. | +| Project context | For `init`, `run`, `restore`, `update`, and `package`, a separate correlated event records bounded categories describing the project family (`dotnet`, `node`, `cpp`, `rust`, `dart`, `hybrid`, `mixed`, or `unknown`), recognized app framework (such as `winui`, `wpf`, `winforms`, `maui`, `electron`, `tauri`, `flutter`, `react-native-windows`, `avalonia`, `uwp`, `windows-app-sdk`, or `other-dotnet`), target kind, detection source, confidence, packaging model, and whether `run` used project or folder execution. It doesn't include project names, paths, repositories, versions, dependency lists, or source. | | `find-ui` usage | For the `winapp find-ui` command only, an additional usage event with non-personal, bounded values: the mode (`search`, `fetch`, or `list`); the selected `--source` (a fixed value — `gallery`, `toolkit`, `reactor`, or `core`); the catalog scenario IDs fetched (e.g. `gallery-tabview-1`), which identify built-in WinUI sample controls, never your code; whether `--json` was used; and result/ID counts. The free-form search query is **never** collected, and any requested IDs that don't match a real catalog entry are counted but **not** collected as text. | | `find-api` usage | For the `winapp find-api` command only, an additional usage event with non-personal, bounded values: the verb (a fixed value — `search`, `members`, `check-property`, `types`, `enums`, `namespaces`, `packages`, `stats`, `projects`, or `refresh`); whether `--json` was used; a result count; and whether the lookup found a match. The free-form arguments — the search query and any type, namespace, or property name — are **never** collected. | | `winapp ui` desktop coordination | For locally executed `winapp ui` commands only, a privacy-minimized summary of how the command shared the desktop with other UI workflows: how the workflow identity was resolved (`Workflow` or `Anonymous` — never the identity itself), the coordination mode (`Observe`, `TurnShared`, or `DesktopExclusive`), how the turn was obtained (new, continuation, queued, handoff-after-idle, or detached), the outcome (completed, cancelled, coordination failure, or corruption recovery), and **coarse buckets** for how long this command waited for the desktop, how many commands were queued, and how long the owning workflow had held its turn. | diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs index b2b3a3e85..2eb33db8e 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs @@ -97,14 +97,14 @@ public async Task Init_ExplicitNestedDirectory_PreservesAncestorConfidence() } [TestMethod] - public void EnabledTelemetry_EmitsProjectContextAtCriticalLevel() + public void EnabledTelemetry_EmitsProjectContextAtMeasureLevel() { ProjectContextEvent.Log( "restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); Assert.AreEqual("ProjectContext_Event", _telemetry.EventNames.Single()); - Assert.AreEqual(LogLevel.Critical, _telemetry.Levels.Single()); + Assert.AreEqual(LogLevel.Measure, _telemetry.Levels.Single()); } [TestMethod] diff --git a/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs index 4a5d6efd6..bd787e787 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/TelemetryTests.cs @@ -8,7 +8,6 @@ using Microsoft.Diagnostics.Telemetry.Internal; using WinApp.Cli.Commands; using WinApp.Cli.Helpers; -using WinApp.Cli.Services; using WinApp.Cli.Telemetry; using WinApp.Cli.Telemetry.Events; @@ -90,56 +89,6 @@ public void CommandEvents_EmitTargetKindInBothPayloads() "ExecutionTargetKind", "sandbox"); } - [TestMethod] - public void ProjectContextEvent_UsesCriticalKeywordWithoutEnablingDiagnostics() - { - using var listener = new CapturingEventListener(ProviderName); - var telemetry = new WinApp.Cli.Telemetry.Telemetry(); - TelemetryFactory.SetOverrideForTesting(telemetry); - try - { - ProjectContextEvent.Log("restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); - ProjectContextEvent.Log("run", () => ProjectContext.Unknown(ProjectTargetKind.BuildOutput)); - telemetry.Log("OtherMeasure", LogLevel.Measure, new ProbeEvent { Detail = "local-only" }); - - var events = listener.WaitForEvents(3); - Assert.IsFalse(telemetry.IsDiagnosticTelemetryOn); - AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, - "Command", "restore"); - AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, - "Command", "run"); - AssertEvent(events, "ProjectContext_Event", EventLevel.Verbose, TelemetryEventSource.CriticalDataKeyword, - "ProjectFamily", "unknown"); - AssertEvent(events, "OtherMeasure", EventLevel.Verbose, EventKeywords.None, - "Detail", "local-only"); - } - finally - { - TelemetryFactory.SetOverrideForTesting(null); - } - } - - [TestMethod] - public void ProjectContextEvent_OptOutSkipsClassificationAndEmission() - { - Environment.SetEnvironmentVariable("WINAPP_CLI_TELEMETRY_OPTOUT", "1"); - using var listener = new CapturingEventListener(ProviderName); - var telemetry = new WinApp.Cli.Telemetry.Telemetry(); - TelemetryFactory.SetOverrideForTesting(telemetry); - try - { - ProjectContextEvent.Log("restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); - ProjectContextEvent.Log("run", () => throw new AssertFailedException("Opt-out must not inspect project metadata.")); - - Assert.IsFalse(telemetry.IsTelemetryOn); - Assert.IsFalse(listener.WaitForEvents(0).Any(e => e.Name == "ProjectContext_Event")); - } - finally - { - TelemetryFactory.SetOverrideForTesting(null); - } - } - [TestMethod] public void AddSensitiveString_IgnoresUnsafeEntriesAndSanitizesEventBaseFields() { From d3b29f87d168c56deee6148b569d4288f2614828 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:52:37 -0700 Subject: [PATCH 3/3] Align project context test with approved classification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs index 2eb33db8e..b2b3a3e85 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ProjectContextCommandTelemetryTests.cs @@ -97,14 +97,14 @@ public async Task Init_ExplicitNestedDirectory_PreservesAncestorConfidence() } [TestMethod] - public void EnabledTelemetry_EmitsProjectContextAtMeasureLevel() + public void EnabledTelemetry_EmitsProjectContextAtCriticalLevel() { ProjectContextEvent.Log( "restore", ProjectContext.Unknown(ProjectTargetKind.Workspace)); Assert.AreEqual("ProjectContext_Event", _telemetry.EventNames.Single()); - Assert.AreEqual(LogLevel.Measure, _telemetry.Levels.Single()); + Assert.AreEqual(LogLevel.Critical, _telemetry.Levels.Single()); } [TestMethod]