diff --git a/API_CHANGES.md b/API_CHANGES.md index c1b3e96a93..633b7e7c71 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -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 diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssueDetectedEvent.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssueDetectedEvent.java index 7f70b996a1..858fa53943 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssueDetectedEvent.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/RawIssueDetectedEvent.java @@ -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) { } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java index 41f409e4c7..ba8e8eb84c 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/embedded/server/ShowFixSuggestionRequestHandler.java @@ -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); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java index fb3799323d..8b587e04e8 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/remediation/aicodefix/AiCodeFixService.java @@ -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; @@ -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) { @@ -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); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java index 89c9fd0f12..1482c9fea3 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/reporting/FindingReportingService.java @@ -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; @@ -67,6 +68,7 @@ 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> issuesPerFileUri = new ConcurrentHashMap<>(); private final Map> securityHotspotsPerFileUri = new ConcurrentHashMap<>(); @@ -74,12 +76,13 @@ public class FindingReportingService { private final Map> 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; } @@ -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); diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java index dc6c281586..735550887a 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/repository/reporting/PreviouslyRaisedFindingsRepository.java @@ -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>> previouslyRaisedIssuesByScopeId = new ConcurrentHashMap<>(); @@ -52,6 +53,24 @@ private static Map> 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> 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> getRaisedIssuesForScope(String scopeId) { return previouslyRaisedIssuesByScopeId.getOrDefault(scopeId, Map.of()); } @@ -70,20 +89,6 @@ private static void resetCacheForFindings(String sc cache.compute(scopeId, (file, issues) -> blankCache); } - public Optional getRaisedIssueWithScopeAndId(String scopeId, UUID issueId) { - return getRaisedIssuesForScope(scopeId).values().stream() - .flatMap(List::stream) - .filter(issue -> issue.getId().equals(issueId)) - .findFirst(); - } - - public Optional getRaisedHotspotWithScopeAndId(String scopeId, UUID hotspotId) { - return getRaisedHotspotsForScope(scopeId).values().stream() - .flatMap(List::stream) - .filter(hotspot -> hotspot.getId().equals(hotspotId)) - .findFirst(); - } - public Optional findRaisedIssueById(UUID issueId) { return previouslyRaisedIssuesByScopeId.values().stream() .flatMap(issuesByUri -> issuesByUri.entrySet().stream() diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryService.java index 9bb46b1b7d..06364dd87c 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryService.java @@ -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; @@ -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)); } diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackingService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackingService.java index 2fb76881c4..30c997ae0d 100644 --- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackingService.java +++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/tracking/TrackingService.java @@ -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); } } diff --git a/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TelemetryRpcServiceDelegate.java b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TelemetryRpcServiceDelegate.java index 7e00cdf7ef..9fa1b5905d 100644 --- a/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TelemetryRpcServiceDelegate.java +++ b/backend/rpc-impl/src/main/java/org/sonarsource/sonarlint/core/rpc/impl/TelemetryRpcServiceDelegate.java @@ -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; @@ -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()); diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionFeedback.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionFeedback.java new file mode 100644 index 0000000000..73af7c0d52 --- /dev/null +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionFeedback.java @@ -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) { +} diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionReceivedCounter.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionReceivedCounter.java index 70ae132023..17ae82c69f 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionReceivedCounter.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryFixSuggestionReceivedCounter.java @@ -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) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClient.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClient.java index 20cef9b12b..c654368f87 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClient.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClient.java @@ -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; @@ -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) { diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorage.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorage.java index 7acf5360e1..3d8d326e6b 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorage.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorage.java @@ -60,6 +60,8 @@ public class TelemetryLocalStorage { private final Map helpAndFeedbackLinkClickedCount; private final Map fixSuggestionReceivedCounter; private final Map> fixSuggestionResolved; + private final Map fixSuggestionFeedback; + private int countIssuesWithPossibleAiFixFromIde; private boolean isFocusOnNewCode; private int codeFocusChangedCount; private int manualAddedBindingsCount; @@ -78,6 +80,7 @@ public class TelemetryLocalStorage { helpAndFeedbackLinkClickedCount = new LinkedHashMap<>(); fixSuggestionReceivedCounter = new LinkedHashMap<>(); fixSuggestionResolved = new LinkedHashMap<>(); + fixSuggestionFeedback = new LinkedHashMap<>(); } public Collection getRaisedIssuesRules() { @@ -143,6 +146,14 @@ public Map> getFixSuggestionR return fixSuggestionResolved; } + public Map getFixSuggestionFeedback() { + return fixSuggestionFeedback; + } + + public int getCountIssuesWithPossibleAiFixFromIde() { + return countIssuesWithPossibleAiFixFromIde; + } + public boolean isFocusOnNewCode() { return isFocusOnNewCode; } @@ -184,6 +195,8 @@ void clearAfterPing() { helpAndFeedbackLinkClickedCount.clear(); fixSuggestionReceivedCounter.clear(); fixSuggestionResolved.clear(); + fixSuggestionFeedback.clear(); + countIssuesWithPossibleAiFixFromIde = 0; codeFocusChangedCount = 0; manualAddedBindingsCount = 0; importedAddedBindingsCount = 0; @@ -294,9 +307,9 @@ public void incrementShowIssueRequestCount() { showIssueRequestsCount++; } - public void fixSuggestionReceived(String suggestionId, AiSuggestionSource aiSuggestionSource, int snippetsCount) { + public void fixSuggestionReceived(String suggestionId, AiSuggestionSource aiSuggestionSource, int snippetsCount, boolean wasGeneratedFromIde) { markSonarLintAsUsedToday(); - this.fixSuggestionReceivedCounter.computeIfAbsent(suggestionId, k -> new TelemetryFixSuggestionReceivedCounter(aiSuggestionSource, snippetsCount)); + this.fixSuggestionReceivedCounter.computeIfAbsent(suggestionId, k -> new TelemetryFixSuggestionReceivedCounter(aiSuggestionSource, snippetsCount, wasGeneratedFromIde)); } public void fixSuggestionResolved(String suggestionId, FixSuggestionStatus status, @Nullable Integer snippetIndex) { @@ -314,6 +327,16 @@ public void fixSuggestionResolved(String suggestionId, FixSuggestionStatus statu () -> fixSuggestionSnippets.add(new TelemetryFixSuggestionResolvedStatus(status, snippetIndex))); } + public void fixSuggestionFeedbackGiven(String suggestionId, boolean feedback) { + markSonarLintAsUsedToday(); + this.fixSuggestionFeedback.computeIfAbsent(suggestionId, k -> new TelemetryFixSuggestionFeedback(feedback)); + } + + public void incrementCountIssuesWithPossibleAiFixFromIde() { + markSonarLintAsUsedToday(); + countIssuesWithPossibleAiFixFromIde++; + } + public int getShowIssueRequestsCount() { return showIssueRequestsCount; } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtils.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtils.java index 03dfb8b759..1238baaeb5 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtils.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtils.java @@ -90,18 +90,22 @@ private static Map toNotifPayload( static TelemetryFixSuggestionPayload[] toFixSuggestionResolvedPayload( Map fixSuggestionReceivedCounter, - Map> fixSuggestionResolved + Map> fixSuggestionResolved, + Map fixSuggestionFeedback ) { return fixSuggestionReceivedCounter.entrySet().stream().map(e -> { var suggestionId = e.getKey(); - var snippetsCount = e.getValue().getSnippetsCount(); - var source = e.getValue().getAiSuggestionsSource(); - var resolvedSnippetStatues = fixSuggestionResolved.getOrDefault(suggestionId, List.of(new TelemetryFixSuggestionResolvedStatus(null, null))); - var resolvedSnippetPayload = resolvedSnippetStatues.stream() + var snippetsCount = e.getValue().snippetsCount(); + var source = e.getValue().aiSuggestionsSource(); + var resolvedSnippetStatus = fixSuggestionResolved.getOrDefault(suggestionId, List.of(new TelemetryFixSuggestionResolvedStatus(null, null))); + var resolvedSnippetPayload = resolvedSnippetStatus.stream() .map(s -> new TelemetryFixSuggestionResolvedPayload(s.getFixSuggestionResolvedStatus(), s.getFixSuggestionResolvedSnippetIndex())).toList(); + var wasGeneratedFromIde = e.getValue().wasGeneratedFromIde(); + var feedback = fixSuggestionFeedback.getOrDefault(suggestionId, new TelemetryFixSuggestionFeedback(null)); + var isFeedbackPositive = feedback.isFeedbackPositive(); - return new TelemetryFixSuggestionPayload(suggestionId, snippetsCount, source, resolvedSnippetPayload); + return new TelemetryFixSuggestionPayload(suggestionId, snippetsCount, source, resolvedSnippetPayload, wasGeneratedFromIde, isFeedbackPositive); }).toArray(TelemetryFixSuggestionPayload[]::new); } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/HotspotPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/HotspotPayload.java index 6f068b1485..c0819c2107 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/HotspotPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/HotspotPayload.java @@ -21,14 +21,6 @@ import com.google.gson.annotations.SerializedName; -public class HotspotPayload { - @SerializedName("open_in_browser_count") - public final int openInBrowserCount; - @SerializedName("status_changed_count") - public final int statusChangedCount; - - public HotspotPayload(int openInBrowserCount, int statusChangedCount) { - this.openInBrowserCount = openInBrowserCount; - this.statusChangedCount = statusChangedCount; - } +public record HotspotPayload(@SerializedName("open_in_browser_count") int openInBrowserCount, + @SerializedName("status_changed_count") int statusChangedCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/IssuePayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/IssuePayload.java index fd8eb1c8be..5a89ea9eb2 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/IssuePayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/IssuePayload.java @@ -22,18 +22,6 @@ import com.google.gson.annotations.SerializedName; import java.util.Set; -public class IssuePayload { - @SerializedName("status_changed_rule_keys") - public final Set statusChangedRuleKeys; - @SerializedName("status_changed_count") - public final int statusChangedCount; - - public IssuePayload(Set statusChangedRuleKeys, int statusChangedCount) { - this.statusChangedRuleKeys = statusChangedRuleKeys; - this.statusChangedCount = statusChangedCount; - } - - public Set getStatusChangedRuleKeys() { - return statusChangedRuleKeys; - } +public record IssuePayload(@SerializedName("status_changed_rule_keys") Set statusChangedRuleKeys, + @SerializedName("status_changed_count") int statusChangedCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShareConnectedModePayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShareConnectedModePayload.java index fd237c6a65..8e6490ea67 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShareConnectedModePayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShareConnectedModePayload.java @@ -22,26 +22,8 @@ import com.google.gson.annotations.SerializedName; import javax.annotation.Nullable; -public class ShareConnectedModePayload { - - @SerializedName("manual_bindings_count") - public final Integer manualAddedBindingsCount; - - @SerializedName("imported_bindings_count") - public final Integer importedAddedBindingsCount; - - @SerializedName("auto_bindings_count") - public final Integer autoAddedBindingsCount; - - @SerializedName("exported_connected_mode_count") - public final Integer exportedConnectedModeCount; - - public ShareConnectedModePayload(@Nullable Integer manualAddedBindingsCount, @Nullable Integer importedAddedBindingsCount, - @Nullable Integer autoAddedBindingsCount, @Nullable Integer exportedConnectedModeCount) { - this.manualAddedBindingsCount = manualAddedBindingsCount; - this.importedAddedBindingsCount = importedAddedBindingsCount; - this.autoAddedBindingsCount = autoAddedBindingsCount; - this.exportedConnectedModeCount = exportedConnectedModeCount; - } - +public record ShareConnectedModePayload(@SerializedName("manual_bindings_count") @Nullable Integer manualAddedBindingsCount, + @SerializedName("imported_bindings_count") @Nullable Integer importedAddedBindingsCount, + @SerializedName("auto_bindings_count") @Nullable Integer autoAddedBindingsCount, + @SerializedName("exported_connected_mode_count") @Nullable Integer exportedConnectedModeCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowHotspotPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowHotspotPayload.java index 034a60b544..c0edb6284c 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowHotspotPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowHotspotPayload.java @@ -21,11 +21,5 @@ import com.google.gson.annotations.SerializedName; -public class ShowHotspotPayload { - @SerializedName("requests_count") - public final int requestsCount; - - public ShowHotspotPayload(int requestsCount) { - this.requestsCount = requestsCount; - } +public record ShowHotspotPayload(@SerializedName("requests_count") int requestsCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowIssuePayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowIssuePayload.java index 9009eb40eb..0f03bad77b 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowIssuePayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/ShowIssuePayload.java @@ -21,11 +21,5 @@ import com.google.gson.annotations.SerializedName; -public class ShowIssuePayload { - @SerializedName("requests_count") - public final int requestsCount; - - public ShowIssuePayload(int requestsCount) { - this.requestsCount = requestsCount; - } +public record ShowIssuePayload(@SerializedName("requests_count") int requestsCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TaintVulnerabilitiesPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TaintVulnerabilitiesPayload.java index 53251550b4..c6867e95cd 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TaintVulnerabilitiesPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TaintVulnerabilitiesPayload.java @@ -21,15 +21,6 @@ import com.google.gson.annotations.SerializedName; -public class TaintVulnerabilitiesPayload { - @SerializedName("investigated_locally_count") - public final int investigatedLocallyCount; - - @SerializedName("investigated_remotely_count") - public final int investigatedRemotelyCount; - - public TaintVulnerabilitiesPayload(int investigatedLocallyCount, int investigatedRemotelyCount) { - this.investigatedLocallyCount = investigatedLocallyCount; - this.investigatedRemotelyCount = investigatedRemotelyCount; - } +public record TaintVulnerabilitiesPayload(@SerializedName("investigated_locally_count") int investigatedLocallyCount, + @SerializedName("investigated_remotely_count") int investigatedRemotelyCount) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryAnalyzerPerformancePayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryAnalyzerPerformancePayload.java index 2563341452..273abba31a 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryAnalyzerPerformancePayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryAnalyzerPerformancePayload.java @@ -23,23 +23,6 @@ import java.math.BigDecimal; import java.util.Map; -public class TelemetryAnalyzerPerformancePayload { - private final String language; - - @SerializedName("rate_per_duration") - private final Map distribution; - - public TelemetryAnalyzerPerformancePayload(String language, Map distribution) { - this.language = language; - this.distribution = distribution; - } - - public String language() { - return language; - } - - public Map distribution() { - return distribution; - } - +public record TelemetryAnalyzerPerformancePayload(String language, + @SerializedName("rate_per_duration") Map distribution) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionPayload.java index 0db3fdd93e..06b850fdfa 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionPayload.java @@ -21,42 +21,13 @@ import com.google.gson.annotations.SerializedName; import java.util.List; +import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.AiSuggestionSource; -public class TelemetryFixSuggestionPayload { - @SerializedName("suggestion_id") - private final String suggestionId; - - @SerializedName("count_snippets") - private final int countSnippets; - - @SerializedName("opened_from") - private final AiSuggestionSource openedFrom; - - @SerializedName("snippets") - private final List snippets; - - public TelemetryFixSuggestionPayload(String suggestionId, int countSnippets, AiSuggestionSource openedFrom, - List snippets) { - this.suggestionId = suggestionId; - this.countSnippets = countSnippets; - this.openedFrom = openedFrom; - this.snippets = snippets; - } - - public String getSuggestionId() { - return suggestionId; - } - - public List getSnippets() { - return snippets; - } - - public int getCountSnippets() { - return countSnippets; - } - - public AiSuggestionSource getOpenedFrom() { - return openedFrom; - } +public record TelemetryFixSuggestionPayload(@SerializedName("suggestion_id") String suggestionId, + @SerializedName("count_snippets") int countSnippets, + @SerializedName("ai_fix_suggestion_provider") AiSuggestionSource aiFixSuggestionProvider, + @SerializedName("snippets") List snippets, + @SerializedName("was_ai_fix_suggestion_generated_from_ide") boolean wasAiFixSuggestionGeneratedFromIde, + @SerializedName("is_feedback_positive") @Nullable Boolean isFeedbackPositive){ } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionResolvedPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionResolvedPayload.java index 16a294ccaa..2713eebbb6 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionResolvedPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryFixSuggestionResolvedPayload.java @@ -23,23 +23,6 @@ import javax.annotation.Nullable; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.FixSuggestionStatus; -public class TelemetryFixSuggestionResolvedPayload { - @SerializedName("status") - private final FixSuggestionStatus status; - - @SerializedName("snippet_index") - private final Integer snippetIndex; - - public TelemetryFixSuggestionResolvedPayload(@Nullable FixSuggestionStatus status, @Nullable Integer snippetIndex) { - this.status = status; - this.snippetIndex = snippetIndex; - } - - public FixSuggestionStatus getStatus() { - return status; - } - - public Integer getSnippetIndex() { - return snippetIndex; - } +public record TelemetryFixSuggestionResolvedPayload(@SerializedName("status") @Nullable FixSuggestionStatus status, + @SerializedName("snippet_index") @Nullable Integer snippetIndex) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsCounterPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsCounterPayload.java index 253f31012c..2017424971 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsCounterPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsCounterPayload.java @@ -21,25 +21,6 @@ import com.google.gson.annotations.SerializedName; -public class TelemetryNotificationsCounterPayload { - - @SerializedName("received") - private final int devNotificationsCount; - - @SerializedName("clicked") - private final int devNotificationsClicked; - - public TelemetryNotificationsCounterPayload(int devNotificationsCount, int devNotificationsClicked) { - this.devNotificationsCount = devNotificationsCount; - this.devNotificationsClicked = devNotificationsClicked; - } - - public int getDevNotificationsClicked() { - return devNotificationsClicked; - } - - public int getDevNotificationsCount() { - return devNotificationsCount; - } - +public record TelemetryNotificationsCounterPayload(@SerializedName("received") int devNotificationsCount, + @SerializedName("clicked") int devNotificationsClicked) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsPayload.java index ea50c195d0..b1495f7a96 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryNotificationsPayload.java @@ -22,23 +22,6 @@ import com.google.gson.annotations.SerializedName; import java.util.Map; -public class TelemetryNotificationsPayload { - private final boolean disabled; - - @SerializedName("count_by_type") - private final Map counters; - - public TelemetryNotificationsPayload(boolean disabled, Map counters) { - this.disabled = disabled; - this.counters = counters; - } - - public boolean disabled() { - return disabled; - } - - public Map counters() { - return counters; - } - +public record TelemetryNotificationsPayload(boolean disabled, + @SerializedName("count_by_type") Map counters) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayload.java index c203a7abc1..a20e2b7f39 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayload.java @@ -107,6 +107,9 @@ public class TelemetryPayload { @SerializedName("ai_fix_suggestions") private final TelemetryFixSuggestionPayload[] aiFixSuggestionsPayload; + @SerializedName("count_issues_with_possible_ai_fix_from_ide") + private final int countIssuesWithPossibleAiFixFromIde; + @SerializedName("cayc") private final CleanAsYouCodePayload cleanAsYouCodePayload; @@ -120,8 +123,7 @@ public TelemetryPayload(long daysSinceInstallation, long daysOfUse, String produ TelemetryAnalyzerPerformancePayload[] analyses, TelemetryNotificationsPayload notifications, ShowHotspotPayload showHotspotPayload, ShowIssuePayload showIssuePayload, TaintVulnerabilitiesPayload taintVulnerabilitiesPayload, TelemetryRulesPayload telemetryRulesPayload, HotspotPayload hotspotPayload, IssuePayload issuePayload, TelemetryHelpAndFeedbackPayload helpAndFeedbackPayload, TelemetryFixSuggestionPayload[] aiFixSuggestionsPayload, - CleanAsYouCodePayload cleanAsYouCodePayload, - ShareConnectedModePayload shareConnectedModePayload, + int countIssuesWithPossibleAiFixFromIde, CleanAsYouCodePayload cleanAsYouCodePayload, ShareConnectedModePayload shareConnectedModePayload, Map additionalAttributes) { this.daysSinceInstallation = daysSinceInstallation; this.daysOfUse = daysOfUse; @@ -147,6 +149,7 @@ public TelemetryPayload(long daysSinceInstallation, long daysOfUse, String produ this.issuePayload = issuePayload; this.helpAndFeedbackPayload = helpAndFeedbackPayload; this.aiFixSuggestionsPayload = aiFixSuggestionsPayload; + this.countIssuesWithPossibleAiFixFromIde = countIssuesWithPossibleAiFixFromIde; this.cleanAsYouCodePayload = cleanAsYouCodePayload; this.shareConnectedModePayload = shareConnectedModePayload; this.additionalAttributes = additionalAttributes; @@ -260,6 +263,10 @@ public OffsetDateTime getInstallTime() { return installTime; } + public int getCountIssuesWithPossibleAiFixFromIde() { + return countIssuesWithPossibleAiFixFromIde; + } + public String toJson() { var gson = new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()) @@ -286,4 +293,5 @@ static JsonObject mergeObjects(JsonObject source, JsonObject target) { } return target; } + } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryRulesPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryRulesPayload.java index 54085ed2e2..f5ade165a4 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryRulesPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryRulesPayload.java @@ -22,22 +22,8 @@ import com.google.gson.annotations.SerializedName; import java.util.Collection; -public class TelemetryRulesPayload { - - @SerializedName("non_default_enabled") - public final Collection nonDefaultEnabled; - @SerializedName("default_disabled") - public final Collection defaultDisabled; - @SerializedName("raised_issues") - public final Collection raisedIssues; - @SerializedName("quick_fix_applied") - public final Collection quickFixesApplied; - - public TelemetryRulesPayload(Collection nonDefaultEnabled, Collection defaultDisabled, Collection raisedIssues, Collection quickFixesApplied) { - this.nonDefaultEnabled = nonDefaultEnabled; - this.defaultDisabled = defaultDisabled; - this.raisedIssues = raisedIssues; - this.quickFixesApplied = quickFixesApplied; - } - +public record TelemetryRulesPayload(@SerializedName("non_default_enabled") Collection nonDefaultEnabled, + @SerializedName("default_disabled") Collection defaultDisabled, + @SerializedName("raised_issues") Collection raisedIssues, + @SerializedName("quick_fix_applied") Collection quickFixesApplied) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/CleanAsYouCodePayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/CleanAsYouCodePayload.java index f9bd9b18d8..e4d9d621ff 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/CleanAsYouCodePayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/CleanAsYouCodePayload.java @@ -21,15 +21,5 @@ import com.google.gson.annotations.SerializedName; -public class CleanAsYouCodePayload { - @SerializedName("new_code_focus") - private final NewCodeFocusPayload newCodeFocusPayload; - - public CleanAsYouCodePayload(NewCodeFocusPayload newCodeFocusPayload) { - this.newCodeFocusPayload = newCodeFocusPayload; - } - - public NewCodeFocusPayload getNewCodePayload() { - return newCodeFocusPayload; - } +public record CleanAsYouCodePayload(@SerializedName("new_code_focus") NewCodeFocusPayload newCodeFocusPayload) { } diff --git a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/NewCodeFocusPayload.java b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/NewCodeFocusPayload.java index 4edcc22bff..d38e2f2240 100644 --- a/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/NewCodeFocusPayload.java +++ b/backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/payload/cayc/NewCodeFocusPayload.java @@ -19,20 +19,5 @@ */ package org.sonarsource.sonarlint.core.telemetry.payload.cayc; -public class NewCodeFocusPayload { - private final boolean enabled; - private final int changes; - - public NewCodeFocusPayload(boolean enabled, int changes) { - this.enabled = enabled; - this.changes = changes; - } - - public boolean isEnabled() { - return enabled; - } - - public int getChanges() { - return changes; - } +public record NewCodeFocusPayload(boolean enabled, int changes) { } diff --git a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClientTests.java b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClientTests.java index 25d2881f9a..98a365ba19 100644 --- a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClientTests.java +++ b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryHttpClientTests.java @@ -52,10 +52,10 @@ class TelemetryHttpClientTests { @RegisterExtension private static final SonarLintLogTester logTester = new SonarLintLogTester(); + private static final String PLATFORM = SystemUtils.OS_NAME; + private static final String ARCHITECTURE = SystemUtils.OS_ARCH; private TelemetryHttpClient underTest; - private static final String platform = SystemUtils.OS_NAME; - private static final String architecture = SystemUtils.OS_ARCH; @RegisterExtension static WireMockExtension telemetryMock = WireMockExtension.newInstance() @@ -79,13 +79,11 @@ void opt_out() { underTest.optOut(new TelemetryLocalStorage(), getTelemetryLiveAttributesDto()); - await().untilAsserted(() -> { - telemetryMock.verify(deleteRequestedFor(urlEqualTo("/")) - .withRequestBody( - equalToJson( - "{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + platform + "\",\"architecture\":\"" + architecture + "\"}", - true, true))); - }); + await().untilAsserted(() -> telemetryMock.verify(deleteRequestedFor(urlEqualTo("/")) + .withRequestBody( + equalToJson( + "{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + PLATFORM + "\",\"architecture\":\"" + ARCHITECTURE + "\"}", + true, true)))); } @Test @@ -101,7 +99,7 @@ void upload_with_telemetry_debug_enabled() { await().untilAsserted(() -> { assertTelemetryUploaded(true); assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.matches("Sending telemetry payload.")); - assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.contains("{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\""+platform+"\",\"architecture\":\""+architecture+"\"")); + assertThat(logTester.logs(Level.INFO)).anyMatch(l -> l.contains("{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\""+ PLATFORM +"\",\"architecture\":\""+ ARCHITECTURE +"\"")); }); } @@ -117,13 +115,13 @@ private void assertTelemetryUploaded(boolean isDebugEnabled) { telemetryMock.verify(postRequestedFor(urlEqualTo("/")) .withRequestBody( equalToJson( - "{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + platform + "\",\"architecture\":\""+architecture+ "\",\"additionalKey\" : \"additionalValue\",\"help_and_feedback\":{\"count_by_link\":{\"docs\":1}}}", + "{\"days_since_installation\":0,\"days_of_use\":0,\"sonarlint_version\":\"version\",\"sonarlint_product\":\"product\",\"ide_version\":\"ideversion\",\"platform\":\"" + PLATFORM + "\",\"architecture\":\""+ ARCHITECTURE + "\",\"additionalKey\" : \"additionalValue\",\"help_and_feedback\":{\"count_by_link\":{\"docs\":1}}}", true, true))); telemetryMock.verify(postRequestedFor(urlEqualTo("/metrics")) .withRequestBody( equalToJson( - "{\"sonarlint_product\":\"product\",\"os\":\"" + platform + "\",\"dimension\":\"installation\",\"metric_values\": [{\"key\":\"shared_connected_mode.manual\",\"value\":\"0\",\"type\":\"integer\",\"granularity\":\"daily\"},{\"key\":\"help_and_feedback.docs\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}]}", + "{\"sonarlint_product\":\"product\",\"os\":\"" + PLATFORM + "\",\"dimension\":\"installation\",\"metric_values\": [{\"key\":\"shared_connected_mode.manual\",\"value\":\"0\",\"type\":\"integer\",\"granularity\":\"daily\"},{\"key\":\"help_and_feedback.docs\",\"value\":\"1\",\"type\":\"integer\",\"granularity\":\"daily\"}]}", true, true))); } diff --git a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManagerTests.java b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManagerTests.java index 310b1a5120..77fd807d13 100644 --- a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManagerTests.java +++ b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryManagerTests.java @@ -215,7 +215,7 @@ void uploadAndClearTelemetry_should_clear_accumulated_data() { data.incrementOpenHotspotInBrowserCount(); data.incrementShowHotspotRequestCount(); data.incrementShowIssueRequestCount(); - data.fixSuggestionReceived("suggestionId", AiSuggestionSource.SONARCLOUD, 2); + data.fixSuggestionReceived("suggestionId", AiSuggestionSource.SONARCLOUD, 2, true); data.fixSuggestionResolved("suggestionId", FixSuggestionStatus.ACCEPTED, 0); data.incrementTaintVulnerabilitiesInvestigatedLocallyCount(); data.incrementTaintVulnerabilitiesInvestigatedRemotelyCount(); diff --git a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtilsTests.java b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtilsTests.java index 33997a73e3..d6a5e124a2 100644 --- a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtilsTests.java +++ b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/TelemetryUtilsTests.java @@ -127,13 +127,13 @@ void dayChanged_with_hours_should_return_true_if_different_day_and_beyond_hours( @Test void should_create_telemetry_fixSuggestions_payload() { var suggestionId1 = UUID.randomUUID().toString(); - var counter1 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 4); + var counter1 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 4, true); var suggestionId2 = UUID.randomUUID().toString(); - var counter2 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 2); + var counter2 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 2, true); var suggestionId3 = UUID.randomUUID().toString(); - var counter3 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 1); + var counter3 = new TelemetryFixSuggestionReceivedCounter(AiSuggestionSource.SONARCLOUD, 1, false); var fixSuggestionReceivedCounter = Map.of( suggestionId1, counter1, @@ -145,31 +145,41 @@ void should_create_telemetry_fixSuggestions_payload() { var fixSuggestionResolvedStatus3 = new TelemetryFixSuggestionResolvedStatus(FixSuggestionStatus.DECLINED, null); var fixSuggestionResolved = Map.of(suggestionId1, List.of(fixSuggestionResolvedStatus1, fixSuggestionResolvedStatus2), suggestionId3, List.of(fixSuggestionResolvedStatus3)); + var fixSuggestionFeedbackGiven = Map.of( + suggestionId1, new TelemetryFixSuggestionFeedback(true), + suggestionId2, new TelemetryFixSuggestionFeedback(false) + ); - var result = TelemetryUtils.toFixSuggestionResolvedPayload(fixSuggestionReceivedCounter, fixSuggestionResolved); + var result = TelemetryUtils.toFixSuggestionResolvedPayload(fixSuggestionReceivedCounter, fixSuggestionResolved, fixSuggestionFeedbackGiven); assertThat(result).hasSize(3); - var resultingSuggestion1 = Arrays.stream(result).filter(s -> s.getSuggestionId().equals(suggestionId1)).findFirst().orElseThrow(); - assertThat(resultingSuggestion1.getSuggestionId()).isEqualTo(suggestionId1); - assertThat(resultingSuggestion1.getOpenedFrom()).isEqualTo(AiSuggestionSource.SONARCLOUD); - assertThat(resultingSuggestion1.getCountSnippets()).isEqualTo(4); - assertThat(resultingSuggestion1.getSnippets()).hasSize(2); - - var resultingSuggestion2 = Arrays.stream(result).filter(s -> s.getSuggestionId().equals(suggestionId2)).findFirst().orElseThrow(); - assertThat(resultingSuggestion2.getSuggestionId()).isEqualTo(suggestionId2); - assertThat(resultingSuggestion2.getOpenedFrom()).isEqualTo(AiSuggestionSource.SONARCLOUD); - assertThat(resultingSuggestion2.getCountSnippets()).isEqualTo(2); - assertThat(resultingSuggestion2.getSnippets()).hasSize(1); - assertThat(resultingSuggestion2.getSnippets().get(0).getStatus()).isNull(); - assertThat(resultingSuggestion2.getSnippets().get(0).getSnippetIndex()).isNull(); - - var resultingSuggestion3 = Arrays.stream(result).filter(s -> s.getSuggestionId().equals(suggestionId3)).findFirst().orElseThrow(); - assertThat(resultingSuggestion3.getSuggestionId()).isEqualTo(suggestionId3); - assertThat(resultingSuggestion3.getOpenedFrom()).isEqualTo(AiSuggestionSource.SONARCLOUD); - assertThat(resultingSuggestion3.getCountSnippets()).isEqualTo(1); - assertThat(resultingSuggestion3.getSnippets()).hasSize(1); - var telemetryFixSuggestionResolvedPayload3 = resultingSuggestion3.getSnippets().get(0); - assertThat(telemetryFixSuggestionResolvedPayload3.getSnippetIndex()).isNull(); - assertThat(telemetryFixSuggestionResolvedPayload3.getStatus()).isEqualTo(FixSuggestionStatus.DECLINED); + var resultingSuggestion1 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId1)).findFirst().orElseThrow(); + assertThat(resultingSuggestion1.suggestionId()).isEqualTo(suggestionId1); + assertThat(resultingSuggestion1.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); + assertThat(resultingSuggestion1.countSnippets()).isEqualTo(4); + assertThat(resultingSuggestion1.snippets()).hasSize(2); + assertThat(resultingSuggestion1.isFeedbackPositive()).isTrue(); + assertThat(resultingSuggestion1.wasAiFixSuggestionGeneratedFromIde()).isTrue(); + + var resultingSuggestion2 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId2)).findFirst().orElseThrow(); + assertThat(resultingSuggestion2.suggestionId()).isEqualTo(suggestionId2); + assertThat(resultingSuggestion2.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); + assertThat(resultingSuggestion2.countSnippets()).isEqualTo(2); + assertThat(resultingSuggestion2.snippets()).hasSize(1); + assertThat(resultingSuggestion2.snippets().get(0).status()).isNull(); + assertThat(resultingSuggestion2.snippets().get(0).snippetIndex()).isNull(); + assertThat(resultingSuggestion2.isFeedbackPositive()).isFalse(); + assertThat(resultingSuggestion2.wasAiFixSuggestionGeneratedFromIde()).isTrue(); + + var resultingSuggestion3 = Arrays.stream(result).filter(s -> s.suggestionId().equals(suggestionId3)).findFirst().orElseThrow(); + assertThat(resultingSuggestion3.suggestionId()).isEqualTo(suggestionId3); + assertThat(resultingSuggestion3.aiFixSuggestionProvider()).isEqualTo(AiSuggestionSource.SONARCLOUD); + assertThat(resultingSuggestion3.countSnippets()).isEqualTo(1); + assertThat(resultingSuggestion3.snippets()).hasSize(1); + var telemetryFixSuggestionResolvedPayload3 = resultingSuggestion3.snippets().get(0); + assertThat(telemetryFixSuggestionResolvedPayload3.snippetIndex()).isNull(); + assertThat(telemetryFixSuggestionResolvedPayload3.status()).isEqualTo(FixSuggestionStatus.DECLINED); + assertThat(resultingSuggestion3.isFeedbackPositive()).isNull(); + assertThat(resultingSuggestion3.wasAiFixSuggestionGeneratedFromIde()).isFalse(); } } diff --git a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayloadTests.java b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayloadTests.java index 122e3aacb1..98b83a9ea8 100644 --- a/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayloadTests.java +++ b/backend/telemetry/src/test/java/org/sonarsource/sonarlint/core/telemetry/payload/TelemetryPayloadTests.java @@ -78,7 +78,7 @@ void testGenerationJson() { var m = new TelemetryPayload(4, 15, "SLI", "2.4", "Pycharm 3.2", "platform", "architecture", true, true, systemTime, installTime, "Windows 10", "1.8.0", "10.5.2", perf, notifPayload, showHotspotPayload, showIssuePayload, taintVulnerabilitiesPayload, rulesPayload, hotspotPayload, issuePayload, helpAndFeedbackPayload, - aiFixSuggestionsPayload, cleanAsYouCodePayload, sharedConnectedModePayload, additionalProps); + aiFixSuggestionsPayload, 1, cleanAsYouCodePayload, sharedConnectedModePayload, additionalProps); var s = m.toJson(); assertThat(s).isEqualTo("{\"days_since_installation\":4," @@ -104,7 +104,8 @@ void testGenerationJson() { + "\"hotspot\":{\"open_in_browser_count\":5,\"status_changed_count\":3}," + "\"issue\":{\"status_changed_rule_keys\":[\"java:S123\"],\"status_changed_count\":1}," + "\"help_and_feedback\":{\"count_by_link\":{\"docs\":5,\"faq\":4}}," - + "\"ai_fix_suggestions\":[{\"suggestion_id\":\"suggestionId1\",\"count_snippets\":1,\"opened_from\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":0},{\"status\":\"DECLINED\",\"snippet_index\":1}]},{\"suggestion_id\":\"suggestionId2\",\"count_snippets\":2,\"opened_from\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":null}]},{\"suggestion_id\":\"suggestionId3\",\"count_snippets\":3,\"opened_from\":\"SONARCLOUD\",\"snippets\":[{\"status\":null,\"snippet_index\":null}]}]," + + "\"ai_fix_suggestions\":[{\"suggestion_id\":\"suggestionId1\",\"count_snippets\":1,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":0},{\"status\":\"DECLINED\",\"snippet_index\":1}],\"was_ai_fix_suggestion_generated_from_ide\":true,\"is_feedback_positive\":true},{\"suggestion_id\":\"suggestionId2\",\"count_snippets\":2,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":\"ACCEPTED\",\"snippet_index\":null}],\"was_ai_fix_suggestion_generated_from_ide\":true,\"is_feedback_positive\":false},{\"suggestion_id\":\"suggestionId3\",\"count_snippets\":3,\"ai_fix_suggestion_provider\":\"SONARCLOUD\",\"snippets\":[{\"status\":null,\"snippet_index\":null}],\"was_ai_fix_suggestion_generated_from_ide\":false,\"is_feedback_positive\":null}]," + + "\"count_issues_with_possible_ai_fix_from_ide\":1," + "\"cayc\":{\"new_code_focus\":{\"enabled\":true,\"changes\":2}}," + "\"shared_connected_mode\":{\"manual_bindings_count\":3,\"imported_bindings_count\":2,\"auto_bindings_count\":1,\"exported_connected_mode_count\":4}," + "\"aString\":\"stringValue\"," @@ -120,18 +121,19 @@ void testGenerationJson() { assertThat(m.notifications().counters()).containsOnlyKeys("QUALITY_GATE", "NEW_ISSUES"); assertThat(m.helpAndFeedbackPayload().getCounters()).containsOnlyKeys("docs", "faq"); assertThat(m.getAiFixSuggestionsPayload()).hasSize(3); - assertThat(m.cleanAsYouCodePayload().getNewCodePayload()) - .extracting(NewCodeFocusPayload::isEnabled, NewCodeFocusPayload::getChanges) + assertThat(m.getCountIssuesWithPossibleAiFixFromIde()).isEqualTo(1); + assertThat(m.cleanAsYouCodePayload().newCodeFocusPayload()) + .extracting(NewCodeFocusPayload::enabled, NewCodeFocusPayload::changes) .containsExactly(true, 2); - assertThat(m.issuePayload().getStatusChangedRuleKeys()).isEqualTo(Set.of("java:S123")); - assertThat(m.issuePayload().statusChangedCount).isEqualTo(1); + assertThat(m.issuePayload().statusChangedRuleKeys()).isEqualTo(Set.of("java:S123")); + assertThat(m.issuePayload().statusChangedCount()).isEqualTo(1); assertThat(m.additionalAttributes()).containsExactlyEntriesOf(additionalProps); - assertThat(m.getShowHotspotPayload().requestsCount).isEqualTo(4); - assertThat(m.getShowIssuePayload().requestsCount).isEqualTo(3); - assertThat(m.getHotspotPayload().openInBrowserCount).isEqualTo(5); - assertThat(m.getHotspotPayload().statusChangedCount).isEqualTo(3); - assertThat(m.getTaintVulnerabilitiesPayload().investigatedLocallyCount).isEqualTo(6); - assertThat(m.getTaintVulnerabilitiesPayload().investigatedRemotelyCount).isEqualTo(7); + assertThat(m.getShowHotspotPayload().requestsCount()).isEqualTo(4); + assertThat(m.getShowIssuePayload().requestsCount()).isEqualTo(3); + assertThat(m.getHotspotPayload().openInBrowserCount()).isEqualTo(5); + assertThat(m.getHotspotPayload().statusChangedCount()).isEqualTo(3); + assertThat(m.getTaintVulnerabilitiesPayload().investigatedLocallyCount()).isEqualTo(6); + assertThat(m.getTaintVulnerabilitiesPayload().investigatedRemotelyCount()).isEqualTo(7); assertRulesPayload(m); assertShareConnectedModePayload(m); assertMetadata(m); @@ -152,30 +154,33 @@ private static void assertMetadata(TelemetryPayload m) { } private static void assertShareConnectedModePayload(TelemetryPayload m) { - assertThat(m.getShareConnectedModePayload().manualAddedBindingsCount).isEqualTo(3); - assertThat(m.getShareConnectedModePayload().importedAddedBindingsCount).isEqualTo(2); - assertThat(m.getShareConnectedModePayload().autoAddedBindingsCount).isEqualTo(1); - assertThat(m.getShareConnectedModePayload().exportedConnectedModeCount).isEqualTo(4); + assertThat(m.getShareConnectedModePayload().manualAddedBindingsCount()).isEqualTo(3); + assertThat(m.getShareConnectedModePayload().importedAddedBindingsCount()).isEqualTo(2); + assertThat(m.getShareConnectedModePayload().autoAddedBindingsCount()).isEqualTo(1); + assertThat(m.getShareConnectedModePayload().exportedConnectedModeCount()).isEqualTo(4); } private static void assertRulesPayload(TelemetryPayload m) { - assertThat(m.getTelemetryRulesPayload().defaultDisabled).containsExactly("disabledRuleKey1", "disabledRuleKey2"); - assertThat(m.getTelemetryRulesPayload().nonDefaultEnabled).containsExactly("enabledRuleKey1", "enabledRuleKey2"); - assertThat(m.getTelemetryRulesPayload().raisedIssues).containsExactly("reportedRuleKey1", "reportedRuleKey2"); - assertThat(m.getTelemetryRulesPayload().quickFixesApplied).containsExactly("quickFixedRuleKey1", "quickFixedRuleKey2"); + assertThat(m.getTelemetryRulesPayload().defaultDisabled()).containsExactly("disabledRuleKey1", "disabledRuleKey2"); + assertThat(m.getTelemetryRulesPayload().nonDefaultEnabled()).containsExactly("enabledRuleKey1", "enabledRuleKey2"); + assertThat(m.getTelemetryRulesPayload().raisedIssues()).containsExactly("reportedRuleKey1", "reportedRuleKey2"); + assertThat(m.getTelemetryRulesPayload().quickFixesApplied()).containsExactly("quickFixedRuleKey1", "quickFixedRuleKey2"); } private static TelemetryFixSuggestionPayload[] getTelemetryFixSuggestionPayloads() { var fixSuggestionPayload1 = new TelemetryFixSuggestionPayload("suggestionId1", 1, AiSuggestionSource.SONARCLOUD, List.of(new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.ACCEPTED, 0), - new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.DECLINED, 1))); + new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.DECLINED, 1)), + true, true); var fixSuggestionPayload2 = new TelemetryFixSuggestionPayload("suggestionId2", 2, AiSuggestionSource.SONARCLOUD, - List.of(new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.ACCEPTED, null))); + List.of(new TelemetryFixSuggestionResolvedPayload(FixSuggestionStatus.ACCEPTED, null)), + true, false); var fixSuggestionPayload3 = new TelemetryFixSuggestionPayload("suggestionId3", 3, AiSuggestionSource.SONARCLOUD, - List.of(new TelemetryFixSuggestionResolvedPayload(null, null))); + List.of(new TelemetryFixSuggestionResolvedPayload(null, null)), + false, null); return new TelemetryFixSuggestionPayload[]{fixSuggestionPayload1, fixSuggestionPayload2, fixSuggestionPayload3}; } diff --git a/medium-tests/src/test/java/mediumtest/TelemetryMediumTests.java b/medium-tests/src/test/java/mediumtest/TelemetryMediumTests.java index e950b1c198..48be4edc26 100644 --- a/medium-tests/src/test/java/mediumtest/TelemetryMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/TelemetryMediumTests.java @@ -37,6 +37,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.FixSuggestionStatus; import org.sonarsource.sonarlint.core.rpc.protocol.client.telemetry.HelpAndFeedbackClickedParams; @@ -347,6 +348,16 @@ void it_should_record_fixSuggestionResolved(SonarLintTestHarness harness) { "\"fixSuggestionResolved\":{\"suggestionId\":[{\"fixSuggestionResolvedStatus\":\"ACCEPTED\",\"fixSuggestionResolvedSnippetIndex\":0}],\"suggestionId2\":[{\"fixSuggestionResolvedStatus\":\"DECLINED\"}]}")); } + @SonarLintTest + void it_should_record_fixSuggestionFeedbackGiven(SonarLintTestHarness harness) { + var backend = setupClientAndBackend(harness); + + backend.getTelemetryService().fixSuggestionFeedbackGiven(new FixSuggestionFeedbackParams("suggestionId", true)); + backend.getTelemetryService().fixSuggestionFeedbackGiven(new FixSuggestionFeedbackParams("suggestionId2", false)); + await().untilAsserted(() -> assertThat(backend.telemetryFilePath()).content().asBase64Decoded().asString().contains( + "\"fixSuggestionFeedback\":{\"suggestionId\":{\"isFeedbackPositive\":true},\"suggestionId2\":{\"isFeedbackPositive\":false}}")); + } + @SonarLintTest void it_should_record_addQuickFixAppliedForRule(SonarLintTestHarness harness) { var backend = setupClientAndBackend(harness); diff --git a/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java b/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java index 88917f0c5f..fc8ef47349 100644 --- a/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java +++ b/medium-tests/src/test/java/mediumtest/issues/OpenFixSuggestionInIdeMediumTests.java @@ -118,7 +118,7 @@ void it_should_update_the_telemetry_on_show_issue(SonarLintTestHarness harness, await().atMost(2, TimeUnit.SECONDS) .untilAsserted(() -> assertThat(backend.telemetryFilePath()) .content().asBase64Decoded().asString() - .contains("\"fixSuggestionReceivedCounter\":{\"eb93b2b4-f7b0-4b5c-9460-50893968c264\":{\"aiSuggestionsSource\":\"SONARCLOUD\",\"snippetsCount\":1}}")); + .contains("\"fixSuggestionReceivedCounter\":{\"eb93b2b4-f7b0-4b5c-9460-50893968c264\":{\"aiSuggestionsSource\":\"SONARCLOUD\",\"snippetsCount\":1,\"wasGeneratedFromIde\":false}}")); } @SonarLintTest diff --git a/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java b/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java index 50e53b19bf..15b5270030 100644 --- a/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java +++ b/medium-tests/src/test/java/mediumtest/remediation/aicodefix/AiCodeFixMediumTest.java @@ -54,7 +54,6 @@ import static utils.AnalysisUtils.createFile; public class AiCodeFixMediumTest { - public static final String XML_SOURCE_CODE_WITH_ISSUE = """ @@ -522,6 +521,43 @@ void it_should_synchronize_the_ai_codefix_settings_from_the_server(SonarLintTest .build())); } + @SonarLintTest + void it_should_register_telemetry(SonarLintTestHarness harness, @TempDir Path baseDir) { + var filePath = createFile(baseDir, "pom.xml", XML_SOURCE_CODE_WITH_ISSUE); + var fileUri = filePath.toUri(); + var server = harness.newFakeSonarCloudServer("organizationKey") + .withProject("projectKey", + project -> project.withBranch("branchName") + .withAiCodeFixSuggestion(suggestion -> suggestion + .withId(UUID.fromString("e51b7bbd-72bc-4008-a4f1-d75583f3dc98")) + .withExplanation("This is the explanation") + .withChange(0, 0, "This is the new code"))) + .start(); + var fakeClient = harness.newFakeClient() + .withInitialFs("configScope", baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), "configScope", false, null, filePath, null, null, true))) + .build(); + var backend = harness.newBackend() + .withConnectedEmbeddedPluginAndEnabledLanguage(TestPlugin.XML) + .withSonarCloudUrl(server.baseUrl()) + .withSonarCloudConnection("connectionId", "organizationKey", true, storage -> storage + .withProject("projectKey", project -> project.withRuleSet("xml", ruleSet -> ruleSet.withActiveRule("xml:S3421", "MAJOR"))) + .withAiCodeFixSettings(aiCodeFix -> aiCodeFix + .withSupportedRules(Set.of("xml:S3421")) + .organizationEligible(true) + .enabledForProjects("projectKey"))) + .withBoundConfigScope("configScope", "connectionId", "projectKey") + .withTelemetryEnabled() + .start(fakeClient); + var issue = analyzeFileAndGetIssue(fileUri, fakeClient, backend, "configScope"); + + backend.getAiCodeFixRpcService().suggestFix(new SuggestFixParams("configScope", issue.getId())).join(); + + assertThat(backend.telemetryFilePath()) + .content().asBase64Decoded().asString() + .contains("\"countIssuesWithPossibleAiFixFromIde\":1") + .contains("\"fixSuggestionReceivedCounter\":{\"e51b7bbd-72bc-4008-a4f1-d75583f3dc98\":{\"aiSuggestionsSource\":\"SONARCLOUD\",\"snippetsCount\":1,\"wasGeneratedFromIde\":true}}"); + } + private Sonarlint.AiCodeFixSettings readAiCodeFixSettings(SonarLintTestRpcServer backend, String connectionId) { var path = backend.getStorageRoot().resolve(encodeForFs(connectionId)).resolve("ai_codefix.pb"); if (path.toFile().exists()) { diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/telemetry/TelemetryRpcService.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/telemetry/TelemetryRpcService.java index 10fc2a14de..2acd336e82 100644 --- a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/telemetry/TelemetryRpcService.java +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/backend/telemetry/TelemetryRpcService.java @@ -28,6 +28,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; @@ -85,6 +86,13 @@ public interface TelemetryRpcService { @JsonNotification void fixSuggestionResolved(FixSuggestionResolvedParams params); + /** + * Users have the possibility to give a feedback on a generated AI CodeFix in the IDE + * true if feedback is positive, false if negative + */ + @JsonNotification + void fixSuggestionFeedbackGiven(FixSuggestionFeedbackParams params); + @JsonNotification void addedManualBindings(); diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionFeedbackParams.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionFeedbackParams.java new file mode 100644 index 0000000000..bd28c5cfb2 --- /dev/null +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionFeedbackParams.java @@ -0,0 +1,38 @@ +/* + * SonarLint Core - RPC Protocol + * 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.rpc.protocol.client.telemetry; + +public class FixSuggestionFeedbackParams { + private final String suggestionId; + private final boolean isFeedbackPositive; + + public FixSuggestionFeedbackParams(String suggestionId, boolean isFeedbackPositive) { + this.suggestionId = suggestionId; + this.isFeedbackPositive = isFeedbackPositive; + } + + public String getSuggestionId() { + return suggestionId; + } + + public boolean getIsFeedbackPositive() { + return isFeedbackPositive; + } +} diff --git a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionReceivedParams.java b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionReceivedParams.java index fa542eac7e..2bd0057954 100644 --- a/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionReceivedParams.java +++ b/rpc-protocol/src/main/java/org/sonarsource/sonarlint/core/rpc/protocol/client/telemetry/FixSuggestionReceivedParams.java @@ -23,11 +23,13 @@ public class FixSuggestionReceivedParams { private final String suggestionId; private final AiSuggestionSource aiSuggestionsSource; private final int snippetsCount; + private final boolean wasGeneratedFromIde; - public FixSuggestionReceivedParams(String suggestionId, AiSuggestionSource aiSuggestionsSource, int snippetsCount) { + public FixSuggestionReceivedParams(String suggestionId, AiSuggestionSource aiSuggestionsSource, int snippetsCount, boolean wasGeneratedFromIde) { this.suggestionId = suggestionId; this.aiSuggestionsSource = aiSuggestionsSource; this.snippetsCount = snippetsCount; + this.wasGeneratedFromIde = wasGeneratedFromIde; } public String getSuggestionId() { @@ -41,4 +43,8 @@ public AiSuggestionSource getAiSuggestionsSource() { public int getSnippetsCount() { return snippetsCount; } + + public boolean wasGeneratedFromIde() { + return wasGeneratedFromIde; + } }