Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLVS-1771 Support fix suggestion telemetry #5978

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
2 changes: 2 additions & 0 deletions src/Core/Telemetry/ITelemetryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ public interface ITelemetryManager
void TaintIssueInvestigatedRemotely();

void LinkClicked(string linkId);

void FixSuggestionResolved(string suggestionId, IEnumerable<bool> changeResolutionStatus);
}
27 changes: 27 additions & 0 deletions src/Integration.UnitTests/Telemetry/TelemetryManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Action<ITelemetrySLCoreService>>());
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<ITelemetrySLCoreService>();
Expand Down
22 changes: 21 additions & 1 deletion src/Integration/Telemetry/TelemetryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<bool> changeResolutionStatus) =>
telemetryHelper.Notify(telemetryService =>
{
foreach (var resolvedParams in ConvertFixSuggestionChangeToResolvedParams(suggestionId, changeResolutionStatus))
{
telemetryService.FixSuggestionResolved(resolvedParams);
}
});

private static IEnumerable<FixSuggestionResolvedParams> ConvertFixSuggestionChangeToResolvedParams(string suggestionId, IEnumerable<bool> changeApplicationStatus) =>
changeApplicationStatus
.Select((status, index) =>
new FixSuggestionResolvedParams(
suggestionId,
status ? FixSuggestionStatus.ACCEPTED : FixSuggestionStatus.DECLINED,
index));

public SlCoreTelemetryStatus GetStatus() => telemetryHelper.GetStatus();

Expand Down
38 changes: 18 additions & 20 deletions src/IssueViz.UnitTests/FixSuggestion/FixSuggestionHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<ILogger>();
telemetryManager = Substitute.For<ITelemetryManager>();
documentNavigator = Substitute.For<IDocumentNavigator>();
textViewEditor = Substitute.For<ITextViewEditor>();
openInIdeConfigScopeValidator = Substitute.For<IOpenInIdeConfigScopeValidator>();
Expand All @@ -66,14 +66,15 @@ public void TestInitialize()
diffViewService = Substitute.For<IDiffViewService>();

testSubject = new FixSuggestionHandler(
threadHandling,
logger,
documentNavigator,
textViewEditor,
openInIdeConfigScopeValidator,
ideWindowService,
fixSuggestionNotification,
diffViewService);
diffViewService,
telemetryManager,
threadHandling,
logger);
MockConfigScopeRoot();
}

Expand All @@ -85,14 +86,16 @@ public void ApplyFixSuggestion_RunsOnUIThread()
{
var threadHandlingMock = Substitute.For<IThreadHandling>();
var testSubjectNew = new FixSuggestionHandler(
threadHandlingMock,
logger,

documentNavigator,
textViewEditor,
openInIdeConfigScopeValidator,
ideWindowService,
fixSuggestionNotification,
diffViewService);
diffViewService,
telemetryManager,
threadHandlingMock,
logger);

testSubjectNew.ApplyFixSuggestion(ConfigScopeId, SuggestionId, IdePath, OneChange);

Expand All @@ -114,6 +117,7 @@ public void ApplyFixSuggestion_OneChangeAccepted_AppliesChange()
fixSuggestionNotification.Clear();
documentNavigator.Open(@"C:\myFile.cs");
diffViewService.ShowDiffView(Arg.Any<ITextBuffer>(), OneChange);
telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is<IEnumerable<bool>>(x => x.SequenceEqual(new []{true})));
textViewEditor.ApplyChanges(Arg.Any<ITextBuffer>(), Arg.Is<IReadOnlyList<FixSuggestionChange>>(x => x.SequenceEqual(OneChange)), true);
logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, ConfigScopeId, SuggestionId);
});
Expand All @@ -135,6 +139,7 @@ public void ApplyFixSuggestion_OneChangeNotAccepted_DoesNotApplyChange()
fixSuggestionNotification.Clear();
documentNavigator.Open(@"C:\myFile.cs");
diffViewService.ShowDiffView(Arg.Any<ITextBuffer>(), OneChange);
telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is<IEnumerable<bool>>(x => x.SequenceEqual(new []{false})));
logger.WriteLine(FixSuggestionResources.DoneProcessingRequest, ConfigScopeId, SuggestionId);
});
textViewEditor.DidNotReceiveWithAnyArgs().ApplyChanges(default, default, default);
Expand Down Expand Up @@ -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<IEnumerable<bool>>(x => x.SequenceEqual(new []{true, true})));
}

[TestMethod]
Expand All @@ -250,6 +256,7 @@ public void ApplyFixSuggestion_TwoChangesAndJustOneAccepted_AppliesJustOne()
testSubject.ApplyFixSuggestion(ConfigScopeId, SuggestionId, IdePath, TwoChanges);

textViewEditor.Received(1).ApplyChanges(textView.TextBuffer, Arg.Is<IReadOnlyList<FixSuggestionChange>>(x => x.SequenceEqual(new List<FixSuggestionChange> { TwoChanges[0] })), abortOnOriginalTextChanged: true);
telemetryManager.FixSuggestionResolved(SuggestionId, Arg.Is<IEnumerable<bool>>(x => x.SequenceEqual(new []{true, false})));
}

private void MockConfigScopeRoot() =>
Expand All @@ -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,
Expand Down
86 changes: 28 additions & 58 deletions src/IssueViz/FixSuggestion/FixSuggestionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<FixSuggestionChange> changes)
{
try
Expand All @@ -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))
Expand All @@ -110,21 +71,30 @@ 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<FixSuggestionChange> changes, string configurationScopeRoot)
private void ApplyAndShowAppliedFixSuggestions(string fixSuggestionId, string idePath, IReadOnlyList<FixSuggestionChange> changes, string configurationScopeRoot)
{
var absoluteFilePath = Path.Combine(configurationScopeRoot, idePath);
var textView = GetFileContent(absoluteFilePath);
if (!ValidateFileExists(textView, absoluteFilePath))
{
return;
}
ApplySuggestedChangesAndFocus(textView, GetFinalizedChanges(textView, changes), absoluteFilePath);
ApplySuggestedChangesAndFocus(textView, GetFinalizedChanges(textView, changes, fixSuggestionId), absoluteFilePath);
}

private FinalizedFixSuggestionChange[] GetFinalizedChanges(ITextView textView, IReadOnlyList<FixSuggestionChange> changes) =>
diffViewService.ShowDiffView(textView.TextBuffer, changes);
private FinalizedFixSuggestionChange[] GetFinalizedChanges(ITextView textView, IReadOnlyList<FixSuggestionChange> 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))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 25 additions & 0 deletions src/SLCore/Service/Telemetry/FixSuggestionResolvedParams.cs
Original file line number Diff line number Diff line change
@@ -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);
Loading