diff --git a/src/Core/Telemetry/ITelemetryManager.cs b/src/Core/Telemetry/ITelemetryManager.cs index 3fc86e1d25..77368460f8 100644 --- a/src/Core/Telemetry/ITelemetryManager.cs +++ b/src/Core/Telemetry/ITelemetryManager.cs @@ -33,4 +33,6 @@ public interface ITelemetryManager void TaintIssueInvestigatedRemotely(); void LinkClicked(string linkId); + + void FixSuggestionResolved(string suggestionId, IEnumerable changeResolutionStatus); } diff --git a/src/Integration.UnitTests/Telemetry/TelemetryManagerTests.cs b/src/Integration.UnitTests/Telemetry/TelemetryManagerTests.cs index 031f5d3286..1fcb10857e 100644 --- a/src/Integration.UnitTests/Telemetry/TelemetryManagerTests.cs +++ b/src/Integration.UnitTests/Telemetry/TelemetryManagerTests.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.Integration.Telemetry; using SonarLint.VisualStudio.SLCore.Service.Telemetry; +using SonarLint.VisualStudio.SLCore.Service.Telemetry.Models; using SonarLint.VisualStudio.TestInfrastructure; using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; @@ -169,6 +170,32 @@ public void LinkClicked_CallsRpcService() }); } + [TestMethod] + public void FixSuggestionResolved_CallsRpcService() + { + const string anySuggestionId = "any suggestion id"; + FixSuggestionResolvedParams[] expected = + [ + new(anySuggestionId, FixSuggestionStatus.ACCEPTED, 0), + new(anySuggestionId, FixSuggestionStatus.DECLINED, 1), + new(anySuggestionId, FixSuggestionStatus.ACCEPTED, 2), + new(anySuggestionId, FixSuggestionStatus.ACCEPTED, 3), + new(anySuggestionId, FixSuggestionStatus.DECLINED, 4), + ]; + + telemetryManager.FixSuggestionResolved(anySuggestionId, [true, false, true, true, false]); + + Received.InOrder(() => + { + telemetryHandler.Notify(Arg.Any>()); + telemetryService.FixSuggestionResolved(expected[0]); + telemetryService.FixSuggestionResolved(expected[1]); + telemetryService.FixSuggestionResolved(expected[2]); + telemetryService.FixSuggestionResolved(expected[3]); + telemetryService.FixSuggestionResolved(expected[4]); + }); + } + private void MockTelemetryService() { telemetryService = Substitute.For(); diff --git a/src/Integration/Telemetry/TelemetryManager.cs b/src/Integration/Telemetry/TelemetryManager.cs index 81777876a3..a3a0e45904 100644 --- a/src/Integration/Telemetry/TelemetryManager.cs +++ b/src/Integration/Telemetry/TelemetryManager.cs @@ -23,6 +23,7 @@ using SonarLint.VisualStudio.Core; using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.SLCore.Service.Telemetry; +using SonarLint.VisualStudio.SLCore.Service.Telemetry.Models; using Language = SonarLint.VisualStudio.SLCore.Common.Models.Language; namespace SonarLint.VisualStudio.Integration.Telemetry; @@ -46,7 +47,26 @@ public TelemetryManager(ISlCoreTelemetryHelper telemetryHelper, IKnownUIContexts knownUiContexts.VBProjectContextChanged += OnVBProjectContextChanged; } - public void QuickFixApplied(string ruleId) => telemetryHelper.Notify(telemetryService => telemetryService.AddQuickFixAppliedForRule(new AddQuickFixAppliedForRuleParams(ruleId))); + public void QuickFixApplied(string ruleId) => + telemetryHelper.Notify(telemetryService => + telemetryService.AddQuickFixAppliedForRule(new AddQuickFixAppliedForRuleParams(ruleId))); + + public void FixSuggestionResolved(string suggestionId, IEnumerable changeResolutionStatus) => + telemetryHelper.Notify(telemetryService => + { + foreach (var resolvedParams in ConvertFixSuggestionChangeToResolvedParams(suggestionId, changeResolutionStatus)) + { + telemetryService.FixSuggestionResolved(resolvedParams); + } + }); + + private static IEnumerable ConvertFixSuggestionChangeToResolvedParams(string suggestionId, IEnumerable changeApplicationStatus) => + changeApplicationStatus + .Select((status, index) => + new FixSuggestionResolvedParams( + suggestionId, + status ? FixSuggestionStatus.ACCEPTED : FixSuggestionStatus.DECLINED, + index)); public SlCoreTelemetryStatus GetStatus() => telemetryHelper.GetStatus(); diff --git a/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs index 7897f824c3..e110b15881 100644 --- a/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs +++ b/src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs @@ -23,14 +23,12 @@ using Microsoft.VisualStudio.Text.Editor; using NSubstitute.ExceptionExtensions; using SonarLint.VisualStudio.Core; +using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.FixSuggestion; using SonarLint.VisualStudio.IssueVisualization.FixSuggestion.DiffView; using SonarLint.VisualStudio.IssueVisualization.OpenInIde; -using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion; -using SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models; using SonarLint.VisualStudio.TestInfrastructure; -using FileEditDto = SonarLint.VisualStudio.SLCore.Listener.FixSuggestion.Models.FileEditDto; namespace SonarLint.VisualStudio.IssueVisualization.UnitTests.FixSuggestion; @@ -48,16 +46,18 @@ public class FixSuggestionHandlerTests private IFixSuggestionNotification fixSuggestionNotification; private IIDEWindowService ideWindowService; private ITextViewEditor textViewEditor; - private ILogger logger; private IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator; - private FixSuggestionHandler testSubject; + private ILogger logger; private IThreadHandling threadHandling; + private ITelemetryManager telemetryManager; + private FixSuggestionHandler testSubject; [TestInitialize] public void TestInitialize() { threadHandling = new NoOpThreadHandler(); logger = Substitute.For(); + telemetryManager = Substitute.For(); documentNavigator = Substitute.For(); textViewEditor = Substitute.For(); openInIdeConfigScopeValidator = Substitute.For(); @@ -66,14 +66,15 @@ public void TestInitialize() diffViewService = Substitute.For(); testSubject = new FixSuggestionHandler( - threadHandling, - logger, documentNavigator, textViewEditor, openInIdeConfigScopeValidator, ideWindowService, fixSuggestionNotification, - diffViewService); + diffViewService, + telemetryManager, + threadHandling, + logger); MockConfigScopeRoot(); } @@ -85,14 +86,16 @@ public void ApplyFixSuggestion_RunsOnUIThread() { var threadHandlingMock = Substitute.For(); var testSubjectNew = new FixSuggestionHandler( - threadHandlingMock, - logger, + documentNavigator, textViewEditor, openInIdeConfigScopeValidator, ideWindowService, fixSuggestionNotification, - diffViewService); + diffViewService, + telemetryManager, + threadHandlingMock, + logger); testSubjectNew.ApplyFixSuggestion(ConfigScopeId, SuggestionId, IdePath, OneChange); @@ -114,6 +117,7 @@ public void ApplyFixSuggestion_OneChangeAccepted_AppliesChange() fixSuggestionNotification.Clear(); documentNavigator.Open(@"C:\myFile.cs"); diffViewService.ShowDiffView(Arg.Any(), OneChange); + telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is>(x => x.SequenceEqual(new []{true}))); textViewEditor.ApplyChanges(Arg.Any(), Arg.Is>(x => x.SequenceEqual(OneChange)), true); logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, ConfigScopeId, SuggestionId); }); @@ -135,6 +139,7 @@ public void ApplyFixSuggestion_OneChangeNotAccepted_DoesNotApplyChange() fixSuggestionNotification.Clear(); documentNavigator.Open(@"C:\myFile.cs"); diffViewService.ShowDiffView(Arg.Any(), OneChange); + telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is>(x => x.SequenceEqual(new []{false}))); logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, ConfigScopeId, SuggestionId); }); textViewEditor.DidNotReceiveWithAnyArgs().ApplyChanges(default, default, default); @@ -239,6 +244,7 @@ public void ApplyFixSuggestion_TwoChanges_ShowsCorrectDiffView() testSubject.ApplyFixSuggestion(ConfigScopeId, SuggestionId, IdePath, TwoChanges); diffViewService.Received(1).ShowDiffView(textView.TextBuffer, TwoChanges); + telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is>(x => x.SequenceEqual(new []{true, true}))); } [TestMethod] @@ -250,6 +256,7 @@ public void ApplyFixSuggestion_TwoChangesAndJustOneAccepted_AppliesJustOne() testSubject.ApplyFixSuggestion(ConfigScopeId, SuggestionId, IdePath, TwoChanges); textViewEditor.Received(1).ApplyChanges(textView.TextBuffer, Arg.Is>(x => x.SequenceEqual(new List { TwoChanges[0] })), abortOnOriginalTextChanged: true); + telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is>(x => x.SequenceEqual(new []{true, false}))); } private void MockConfigScopeRoot() => @@ -276,15 +283,6 @@ private ITextView MockOpenFile() return textView; } - private static ShowFixSuggestionParams CreateFixSuggestionParams( - - params ChangesDto[] changes) - { - var fixSuggestion = new FixSuggestionDto(SuggestionId, "refactor", new FileEditDto(IdePath, changes.ToList())); - var suggestionParams = new ShowFixSuggestionParams(ConfigScopeId, "key", fixSuggestion); - return suggestionParams; - } - private static FixSuggestionChange CreateChanges( int startLine, int endLine, diff --git a/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs b/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs index eced81acc3..332ac75be7 100644 --- a/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs +++ b/src/IssueViz/FixSuggestion/FixSuggestionHandler.cs @@ -22,7 +22,7 @@ using System.IO; using Microsoft.VisualStudio.Text.Editor; using SonarLint.VisualStudio.Core; -using SonarLint.VisualStudio.Infrastructure.VS; +using SonarLint.VisualStudio.Core.Telemetry; using SonarLint.VisualStudio.IssueVisualization.Editor; using SonarLint.VisualStudio.IssueVisualization.FixSuggestion.DiffView; using SonarLint.VisualStudio.IssueVisualization.OpenInIde; @@ -31,58 +31,19 @@ namespace SonarLint.VisualStudio.IssueVisualization.FixSuggestion; [Export(typeof(IFixSuggestionHandler))] [PartCreationPolicy(CreationPolicy.Shared)] -public class FixSuggestionHandler : IFixSuggestionHandler +[method: ImportingConstructor] +internal class FixSuggestionHandler( + IDocumentNavigator documentNavigator, + ITextViewEditor textViewEditor, + IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator, + IIDEWindowService ideWindowService, + IFixSuggestionNotification fixSuggestionNotification, + IDiffViewService diffViewService, + ITelemetryManager telemetryManager, + IThreadHandling threadHandling, + ILogger logger) + : IFixSuggestionHandler { - private readonly IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator; - private readonly IIDEWindowService ideWindowService; - private readonly IFixSuggestionNotification fixSuggestionNotification; - private readonly IDiffViewService diffViewService; - private readonly IThreadHandling threadHandling; - private readonly ILogger logger; - private readonly IDocumentNavigator documentNavigator; - private readonly ITextViewEditor textViewEditor; - - [ImportingConstructor] - internal FixSuggestionHandler( - ILogger logger, - IDocumentNavigator documentNavigator, - ITextViewEditor textViewEditor, - IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator, - IIDEWindowService ideWindowService, - IFixSuggestionNotification fixSuggestionNotification, - IDiffViewService diffViewService) : - this( - ThreadHandling.Instance, - logger, - documentNavigator, - textViewEditor, - openInIdeConfigScopeValidator, - ideWindowService, - fixSuggestionNotification, - diffViewService) - { - } - - internal FixSuggestionHandler( - IThreadHandling threadHandling, - ILogger logger, - IDocumentNavigator documentNavigator, - ITextViewEditor textViewEditor, - IOpenInIdeConfigScopeValidator openInIdeConfigScopeValidator, - IIDEWindowService ideWindowService, - IFixSuggestionNotification fixSuggestionNotification, - IDiffViewService diffViewService) - { - this.threadHandling = threadHandling; - this.logger = logger; - this.documentNavigator = documentNavigator; - this.textViewEditor = textViewEditor; - this.openInIdeConfigScopeValidator = openInIdeConfigScopeValidator; - this.ideWindowService = ideWindowService; - this.fixSuggestionNotification = fixSuggestionNotification; - this.diffViewService = diffViewService; - } - public void ApplyFixSuggestion(string configScopeId, string fixSuggestionId, string idePath, IReadOnlyList changes) { try @@ -98,7 +59,7 @@ public void ApplyFixSuggestion(string configScopeId, string fixSuggestionId, str return; } - threadHandling.RunOnUIThread(() => ApplyAndShowAppliedFixSuggestions(idePath, changes, configurationScopeRoot)); + threadHandling.RunOnUIThread(() => ApplyAndShowAppliedFixSuggestions(fixSuggestionId, idePath, changes, configurationScopeRoot)); logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, configScopeId, fixSuggestionId); } catch (Exception exception) when (!ErrorHandler.IsCriticalException(exception)) @@ -110,7 +71,7 @@ public void ApplyFixSuggestion(string configScopeId, string fixSuggestionId, str private bool ValidateConfiguration(string configurationScopeId, out string configurationScopeRoot, out string failureReason) => openInIdeConfigScopeValidator.TryGetConfigurationScopeRoot(configurationScopeId, out configurationScopeRoot, out failureReason); - private void ApplyAndShowAppliedFixSuggestions(string idePath, IReadOnlyList changes, string configurationScopeRoot) + private void ApplyAndShowAppliedFixSuggestions(string fixSuggestionId, string idePath, IReadOnlyList changes, string configurationScopeRoot) { var absoluteFilePath = Path.Combine(configurationScopeRoot, idePath); var textView = GetFileContent(absoluteFilePath); @@ -118,13 +79,22 @@ private void ApplyAndShowAppliedFixSuggestions(string idePath, IReadOnlyList changes) => - diffViewService.ShowDiffView(textView.TextBuffer, changes); + private FinalizedFixSuggestionChange[] GetFinalizedChanges(ITextView textView, IReadOnlyList changes, string fixSuggestionId) + { + var finalizedFixSuggestionChanges = diffViewService.ShowDiffView(textView.TextBuffer, changes); + + telemetryManager.FixSuggestionResolved(fixSuggestionId, finalizedFixSuggestionChanges.Select(x => x.IsAccepted)); + + return finalizedFixSuggestionChanges; + } - private void ApplySuggestedChangesAndFocus(ITextView textView, FinalizedFixSuggestionChange[] finalizedFixSuggestionChanges, string filePath) + private void ApplySuggestedChangesAndFocus( + ITextView textView, + FinalizedFixSuggestionChange[] finalizedFixSuggestionChanges, + string filePath) { if (!finalizedFixSuggestionChanges.Any(x => x.IsAccepted)) { diff --git a/src/SLCore.UnitTests/Service/Telemetry/FixSuggestionResolvedParamsTests.cs b/src/SLCore.UnitTests/Service/Telemetry/FixSuggestionResolvedParamsTests.cs new file mode 100644 index 0000000000..12f1b5643c --- /dev/null +++ b/src/SLCore.UnitTests/Service/Telemetry/FixSuggestionResolvedParamsTests.cs @@ -0,0 +1,37 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using SonarLint.VisualStudio.SLCore.Service.Telemetry; +using SonarLint.VisualStudio.SLCore.Service.Telemetry.Models; + +namespace SonarLint.VisualStudio.SLCore.UnitTests.Service.Telemetry; + +[TestClass] +public class FixSuggestionResolvedParamsTests +{ + [DataRow("sgstid", FixSuggestionStatus.ACCEPTED, null, """{"suggestionId":"sgstid","status":"ACCEPTED","snippetIndex":null}""")] + [DataRow("sgstid2", FixSuggestionStatus.DECLINED, 12, """{"suggestionId":"sgstid2","status":"DECLINED","snippetIndex":12}""")] + [DataTestMethod] + public void SerializedAsExpected(string suggestionId, FixSuggestionStatus status, int? index, string expectedSerialized) + { + JsonConvert.SerializeObject(new FixSuggestionResolvedParams(suggestionId, status, index)).Should().Be(expectedSerialized); + } +} diff --git a/src/SLCore/Service/Telemetry/FixSuggestionResolvedParams.cs b/src/SLCore/Service/Telemetry/FixSuggestionResolvedParams.cs new file mode 100644 index 0000000000..206d9b84a8 --- /dev/null +++ b/src/SLCore/Service/Telemetry/FixSuggestionResolvedParams.cs @@ -0,0 +1,25 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using SonarLint.VisualStudio.SLCore.Service.Telemetry.Models; + +namespace SonarLint.VisualStudio.SLCore.Service.Telemetry; + +public record FixSuggestionResolvedParams(string suggestionId, FixSuggestionStatus status, int? snippetIndex); diff --git a/src/SLCore/Service/Telemetry/ITelemetrySLCoreService.cs b/src/SLCore/Service/Telemetry/ITelemetrySLCoreService.cs index f902ae6e54..9186f4a24f 100644 --- a/src/SLCore/Service/Telemetry/ITelemetrySLCoreService.cs +++ b/src/SLCore/Service/Telemetry/ITelemetrySLCoreService.cs @@ -35,6 +35,7 @@ public interface ITelemetrySLCoreService : ISLCoreService void TaintVulnerabilitiesInvestigatedRemotely(); void AddReportedRules(AddReportedRulesParams parameters); void AddQuickFixAppliedForRule(AddQuickFixAppliedForRuleParams parameters); + void FixSuggestionResolved(FixSuggestionResolvedParams parameters); void HelpAndFeedbackLinkClicked(HelpAndFeedbackClickedParams parameters); void AddedManualBindings(); void AddedImportedBindings(); diff --git a/src/SLCore/Service/Telemetry/Models/FixSuggestionStatus.cs b/src/SLCore/Service/Telemetry/Models/FixSuggestionStatus.cs new file mode 100644 index 0000000000..d05c4c001f --- /dev/null +++ b/src/SLCore/Service/Telemetry/Models/FixSuggestionStatus.cs @@ -0,0 +1,29 @@ +/* + * SonarLint for Visual Studio + * Copyright (C) 2016-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SonarLint.VisualStudio.SLCore.Service.Telemetry.Models; + +[JsonConverter(typeof(StringEnumConverter))] +public enum FixSuggestionStatus { + ACCEPTED, DECLINED +}