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

SLCORE-1184 Collect telemetry on AI CodeFix in the IDE #1256

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions API_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

* Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.AiCodeFixRpcService` class, containing a `suggestFix(SuggestFixParams)` method.
* Introduce a new `isAiCodeFixable` method in `org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto`.
* Introduce a new `fixSuggestionFeedbackGiven` method to `org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.TelemetryRpcService`
* Users have the possibility to give a positive or negative feedback on the suggested AI CodeFix in the IDE
* It should only be used for suggestions generated from the IDE
* It is not mandatory to give a feedback

## Deprecation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,5 @@

import java.util.UUID;

public class RawIssueDetectedEvent {
private final String configurationScopeId;
private final UUID analysisId;
private final RawIssue detectedIssue;

public RawIssueDetectedEvent(String configurationScopeId, UUID analysisId, RawIssue detectedIssue) {
this.configurationScopeId = configurationScopeId;
this.analysisId = analysisId;
this.detectedIssue = detectedIssue;
}

public String getConfigurationScopeId() {
return configurationScopeId;
}

public UUID getAnalysisId() {
return analysisId;
}

public RawIssue getDetectedIssue() {
return detectedIssue;
}
public record RawIssueDetectedEvent(String configurationScopeId, UUID analysisId, RawIssue detectedIssue) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public void handle(ClassicHttpRequest request, ClassicHttpResponse response, Htt
}
telemetryService.fixSuggestionReceived(new FixSuggestionReceivedParams(showFixSuggestionQuery.getFixSuggestion().suggestionId,
showFixSuggestionQuery.isSonarCloud ? AiSuggestionSource.SONARCLOUD : AiSuggestionSource.SONARQUBE,
showFixSuggestionQuery.fixSuggestion.fileEdit.changes.size()));
showFixSuggestionQuery.fixSuggestion.fileEdit.changes.size(), false));

AssistCreatingConnectionParams serverConnectionParams = createAssistServerConnectionParams(showFixSuggestionQuery, sonarCloudActiveEnvironment);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@
import org.sonarsource.sonarlint.core.repository.reporting.RaisedIssue;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixChangeDto;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.remediation.aicodefix.SuggestFixResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionReceivedParams;
import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionRequestBodyDto;
import org.sonarsource.sonarlint.core.serverapi.fixsuggestions.AiSuggestionResponseBodyDto;
import org.sonarsource.sonarlint.core.storage.StorageService;
import org.sonarsource.sonarlint.core.telemetry.TelemetryService;

import static java.util.Objects.requireNonNull;
import static org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode.CONFIG_SCOPE_NOT_BOUND;
Expand All @@ -53,15 +56,18 @@ public class AiCodeFixService {
private final PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository;
private final ClientFileSystemService clientFileSystemService;
private final StorageService storageService;
private final TelemetryService telemetryService;

public AiCodeFixService(ConnectionConfigurationRepository connectionRepository, ConfigurationRepository configurationRepository, ConnectionManager connectionManager,
PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, ClientFileSystemService clientFileSystemService, StorageService storageService) {
PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, ClientFileSystemService clientFileSystemService, StorageService storageService,
TelemetryService telemetryService) {
this.connectionRepository = connectionRepository;
this.configurationRepository = configurationRepository;
this.connectionManager = connectionManager;
this.previouslyRaisedFindingsRepository = previouslyRaisedFindingsRepository;
this.clientFileSystemService = clientFileSystemService;
this.storageService = storageService;
this.telemetryService = telemetryService;
}

public SuggestFixResponse suggestFix(String configurationScopeId, UUID issueId, SonarLintCancelMonitor cancelMonitor) {
Expand All @@ -73,8 +79,19 @@ public SuggestFixResponse suggestFix(String configurationScopeId, UUID issueId,
if (!aiCodeFixFeature.map(feature -> feature.isFixable(issue)).orElse(false)) {
throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "The provided issue cannot be fixed", issueId));
}
return serverApi.fixSuggestions().getAiSuggestion(toDto(sonarQubeCloudBinding.organizationKey, sonarQubeCloudBinding.binding().sonarProjectKey(), issue),

var fixResponseDto = serverApi.fixSuggestions().getAiSuggestion(toDto(sonarQubeCloudBinding.organizationKey, sonarQubeCloudBinding.binding().sonarProjectKey(), issue),
cancelMonitor);

telemetryService.fixSuggestionReceived(new FixSuggestionReceivedParams(
fixResponseDto.id().toString(),
AiSuggestionSource.SONARCLOUD,
fixResponseDto.changes().size(),
// As of today, this is always true since suggestFix is only called by the clients
true
));

return fixResponseDto;
})
.orElseThrow(() -> new ResponseErrorException(new ResponseError(ISSUE_NOT_FOUND, "The provided issue does not exist", issueId))));
return adapt(responseBodyDto);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaiseIssuesParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto;
import org.sonarsource.sonarlint.core.telemetry.TelemetryService;
import org.sonarsource.sonarlint.core.tracking.TrackedIssue;
import org.sonarsource.sonarlint.core.tracking.streaming.Alarm;

Expand All @@ -67,19 +68,21 @@ public class FindingReportingService {
private final NewCodeService newCodeService;
private final SeverityModeService severityModeService;
private final AiCodeFixService aiCodeFixService;
private final TelemetryService telemetryService;
private final PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository;
private final Map<URI, Collection<TrackedIssue>> issuesPerFileUri = new ConcurrentHashMap<>();
private final Map<URI, Collection<TrackedIssue>> securityHotspotsPerFileUri = new ConcurrentHashMap<>();
private final Map<String, Alarm> streamingTriggeringAlarmByConfigScopeId = new ConcurrentHashMap<>();
private final Map<UUID, Set<URI>> filesPerAnalysis = new ConcurrentHashMap<>();

public FindingReportingService(SonarLintRpcClient client, ConfigurationRepository configurationRepository, NewCodeService newCodeService, SeverityModeService severityModeService,
AiCodeFixService aiCodeFixService, PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository) {
AiCodeFixService aiCodeFixService, PreviouslyRaisedFindingsRepository previouslyRaisedFindingsRepository, TelemetryService telemetryService) {
this.client = client;
this.configurationRepository = configurationRepository;
this.newCodeService = newCodeService;
this.severityModeService = severityModeService;
this.aiCodeFixService = aiCodeFixService;
this.telemetryService = telemetryService;
this.previouslyRaisedFindingsRepository = previouslyRaisedFindingsRepository;
}

Expand Down Expand Up @@ -144,6 +147,7 @@ public void reportTrackedFindings(String configurationScopeId, UUID analysisId,
var isMQRMode = severityModeService.isMQRModeForConnection(connectionId);
var aiCodeFixFeature = effectiveBinding.flatMap(aiCodeFixService::getFeature);
var issuesToRaise = getIssuesToRaise(issuesToReport, newCodeDefinition, isMQRMode, aiCodeFixFeature);
aiCodeFixFeature.ifPresent(f -> previouslyRaisedFindingsRepository.countAiFixableIssuesForTelemetry(configurationScopeId, issuesToRaise, telemetryService));
var hotspotsToRaise = getHotspotsToRaise(hotspotsToReport, newCodeDefinition, isMQRMode);
updateRaisedFindingsCacheAndNotifyClient(configurationScopeId, analysisId, issuesToRaise, hotspotsToRaise, false);
filesPerAnalysis.remove(analysisId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.sonarsource.sonarlint.core.rpc.protocol.client.hotspot.RaisedHotspotDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedFindingDto;
import org.sonarsource.sonarlint.core.rpc.protocol.client.issue.RaisedIssueDto;
import org.sonarsource.sonarlint.core.telemetry.TelemetryService;

public class PreviouslyRaisedFindingsRepository {
private final Map<String, Map<URI, List<RaisedIssueDto>>> previouslyRaisedIssuesByScopeId = new ConcurrentHashMap<>();
Expand All @@ -52,6 +53,24 @@ private static <F extends RaisedFindingDto> Map<URI, List<F>> addOrReplaceFindin
return findingsPerFile;
}

/**
* Increment the telemetry counter for issues that are AI fixable
* Avoid incrementing for already known issues, to have a more relevant counter
*/
public void countAiFixableIssuesForTelemetry(String scopeId, Map<URI, List<RaisedIssueDto>> raisedFindings, TelemetryService telemetryService) {
var previousFindings = previouslyRaisedIssuesByScopeId.get(scopeId);
if (!previousFindings.isEmpty()) {
raisedFindings.forEach((uri, issues) -> {
var previousFindingsForFile = previousFindings.get(uri);
if (!previousFindings.isEmpty()) {
issues.stream()
.filter(i -> i.isAiCodeFixable() && !previousFindingsForFile.contains(i))
.forEach(i -> telemetryService.fixSuggestionApplicable());
}
});
}
}

public Map<URI, List<RaisedIssueDto>> getRaisedIssuesForScope(String scopeId) {
return previouslyRaisedIssuesByScopeId.getOrDefault(scopeId, Map.of());
}
Expand All @@ -70,20 +89,6 @@ private static <F extends RaisedFindingDto> void resetCacheForFindings(String sc
cache.compute(scopeId, (file, issues) -> blankCache);
}

public Optional<RaisedIssueDto> getRaisedIssueWithScopeAndId(String scopeId, UUID issueId) {
return getRaisedIssuesForScope(scopeId).values().stream()
.flatMap(List::stream)
.filter(issue -> issue.getId().equals(issueId))
.findFirst();
}

public Optional<RaisedHotspotDto> getRaisedHotspotWithScopeAndId(String scopeId, UUID hotspotId) {
return getRaisedHotspotsForScope(scopeId).values().stream()
.flatMap(List::stream)
.filter(hotspot -> hotspot.getId().equals(hotspotId))
.findFirst();
}

public Optional<RaisedIssue> findRaisedIssueById(UUID issueId) {
return previouslyRaisedIssuesByScopeId.values().stream()
.flatMap(issuesByUri -> issuesByUri.entrySet().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.telemetry.GetStatusResponse;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionFeedbackParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionReceivedParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionResolvedParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.HelpAndFeedbackClickedParams;
Expand Down Expand Up @@ -160,13 +161,26 @@ public void helpAndFeedbackLinkClicked(HelpAndFeedbackClickedParams params) {
}

public void fixSuggestionReceived(FixSuggestionReceivedParams params) {
updateTelemetry(localStorage -> localStorage.fixSuggestionReceived(params.getSuggestionId(), params.getAiSuggestionsSource(), params.getSnippetsCount()));
updateTelemetry(localStorage -> localStorage.fixSuggestionReceived(
params.getSuggestionId(),
params.getAiSuggestionsSource(),
params.getSnippetsCount(),
params.wasGeneratedFromIde())
);
}

public void fixSuggestionResolved(FixSuggestionResolvedParams params) {
updateTelemetry(localStorage -> localStorage.fixSuggestionResolved(params.getSuggestionId(), params.getStatus(), params.getSnippetIndex()));
}

public void fixSuggestionFeedbackGiven(FixSuggestionFeedbackParams params) {
updateTelemetry(localStorage -> localStorage.fixSuggestionFeedbackGiven(params.getSuggestionId(), params.getIsFeedbackPositive()));
}

public void fixSuggestionApplicable() {
updateTelemetry(TelemetryLocalStorage::incrementCountIssuesWithPossibleAiFixFromIde);
}

public void smartNotificationsReceived(String eventType) {
updateTelemetry(localStorage -> localStorage.incrementDevNotificationsCount(eventType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,18 @@ public void onAnalysisStarted(AnalysisStartedEvent event) {

@EventListener
public void onIssueDetected(RawIssueDetectedEvent event) {
var analysisId = event.getAnalysisId();
var analysisId = event.analysisId();
var matchingSession = matchingSessionByAnalysisId.get(analysisId);
if (matchingSession == null) {
// an issue was detected outside any analysis, this normally shouldn't happen
return;
}
var detectedIssue = event.getDetectedIssue();
var detectedIssue = event.detectedIssue();
var isSupported = detectedIssue.isInFile();
if (isSupported) {
// we don't support global issues for now
var trackedIssue = matchingSession.matchWithKnownFinding(requireNonNull(detectedIssue.getIdeRelativePath()), detectedIssue);
reportingService.streamIssue(event.getConfigurationScopeId(), analysisId, trackedIssue);
reportingService.streamIssue(event.configurationScopeId(), analysisId, trackedIssue);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AddReportedRulesParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AnalysisDoneOnSingleLanguageParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.DevNotificationsClickedParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionFeedbackParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionResolvedParams;
import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.HelpAndFeedbackClickedParams;
import org.sonarsource.sonarlint.core.telemetry.TelemetryService;
Expand Down Expand Up @@ -96,6 +97,11 @@ public void fixSuggestionResolved(FixSuggestionResolvedParams params) {
notify(() -> getBean(TelemetryService.class).fixSuggestionResolved(params));
}

@Override
public void fixSuggestionFeedbackGiven(FixSuggestionFeedbackParams params) {
notify(() -> getBean(TelemetryService.class).fixSuggestionFeedbackGiven(params));
}

@Override
public void addedManualBindings() {
notify(() -> getBean(TelemetryService.class).addedManualBindings());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* SonarLint Core - Telemetry
* 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.
*/
package org.sonarsource.sonarlint.core.telemetry;

import javax.annotation.Nullable;

public record TelemetryFixSuggestionFeedback(@Nullable Boolean isFeedbackPositive) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,7 @@

import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource;

public class TelemetryFixSuggestionReceivedCounter {
private final AiSuggestionSource aiSuggestionsSource;
private final int snippetsCount;

public TelemetryFixSuggestionReceivedCounter(AiSuggestionSource aiSuggestionSource, int snippetsCount) {
this.aiSuggestionsSource = aiSuggestionSource;
this.snippetsCount = snippetsCount;
}

public AiSuggestionSource getAiSuggestionsSource() {
return aiSuggestionsSource;
}

public int getSnippetsCount() {
return snippetsCount;
}
public record TelemetryFixSuggestionReceivedCounter(AiSuggestionSource aiSuggestionsSource,
int snippetsCount,
boolean wasGeneratedFromIde) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ private TelemetryPayload createPayload(TelemetryLocalStorage data, TelemetryLive
var telemetryRulesPayload = new TelemetryRulesPayload(telemetryLiveAttrs.getNonDefaultEnabledRules(),
telemetryLiveAttrs.getDefaultDisabledRules(), data.getRaisedIssuesRules(), data.getQuickFixesApplied());
var helpAndFeedbackPayload = new TelemetryHelpAndFeedbackPayload(data.getHelpAndFeedbackLinkClickedCounter());
var fixSuggestionPayload = TelemetryUtils.toFixSuggestionResolvedPayload(data.getFixSuggestionReceivedCounter(), data.getFixSuggestionResolved());
var fixSuggestionPayload = TelemetryUtils.toFixSuggestionResolvedPayload(
data.getFixSuggestionReceivedCounter(),
data.getFixSuggestionResolved(),
data.getFixSuggestionFeedback()
);
var countIssuesWithPossibleAiFixFromIde = data.getCountIssuesWithPossibleAiFixFromIde();
var cleanAsYouCodePayload = new CleanAsYouCodePayload(new NewCodeFocusPayload(data.isFocusOnNewCode(), data.getCodeFocusChangedCount()));

ShareConnectedModePayload shareConnectedModePayload;
Expand All @@ -137,8 +142,7 @@ private TelemetryPayload createPayload(TelemetryLocalStorage data, TelemetryLive
telemetryLiveAttrs.usesConnectedMode(), telemetryLiveAttrs.usesSonarCloud(), systemTime, data.installTime(), platform, jre,
telemetryLiveAttrs.getNodeVersion(), analyzers, notifications, showHotspotPayload, showIssuePayload,
taintVulnerabilitiesPayload, telemetryRulesPayload, hotspotPayload, issuePayload, helpAndFeedbackPayload,
fixSuggestionPayload, cleanAsYouCodePayload, shareConnectedModePayload,
mergedAdditionalAttributes);
fixSuggestionPayload, countIssuesWithPossibleAiFixFromIde, cleanAsYouCodePayload, shareConnectedModePayload, mergedAdditionalAttributes);
}

private TelemetryMetricsPayload createMetricsPayload(TelemetryLocalStorage data, TelemetryLiveAttributes telemetryLiveAttrs) {
Expand Down
Loading