diff --git a/sonar-python-plugin/src/main/java/org/sonar/plugins/python/PythonSensor.java b/sonar-python-plugin/src/main/java/org/sonar/plugins/python/PythonSensor.java index 6dbe1ff3cc..71bf237fa1 100644 --- a/sonar-python-plugin/src/main/java/org/sonar/plugins/python/PythonSensor.java +++ b/sonar-python-plugin/src/main/java/org/sonar/plugins/python/PythonSensor.java @@ -68,17 +68,14 @@ public final class PythonSensor implements Sensor { static final String UNSET_VERSION_WARNING = "Your code is analyzed as compatible with all Python 3 versions by default." + " You can get a more precise analysis by setting the exact Python version in your configuration via the parameter \"sonar.python.version\""; + private final SensorTelemetryStorage sensorTelemetryStorage; + /** * Constructor to be used by pico if neither PythonCustomRuleRepository nor PythonIndexer are to be found and injected. */ public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter, AnalysisWarningsWrapper analysisWarnings) { - this(fileLinesContextFactory, checkFactory, noSonarFilter, null, null, null, analysisWarnings); - } - - public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter, - PythonCustomRuleRepository[] customRuleRepositories, AnalysisWarningsWrapper analysisWarnings) { - this(fileLinesContextFactory, checkFactory, noSonarFilter, customRuleRepositories, null, null, analysisWarnings); + this(fileLinesContextFactory, checkFactory, noSonarFilter, null, null, null, analysisWarnings, new SensorTelemetryStorage()); } public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter, @@ -86,12 +83,12 @@ public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactor // ^^ This constructor implicitly assumes that a PythonIndexer and a SonarLintCache are always available at the same time. // In practice, this is currently the case, since both are provided by PythonPlugin under the same conditions. // See also PythonPlugin::SonarLintPluginAPIManager::addSonarlintPythonIndexer. - this(fileLinesContextFactory, checkFactory, noSonarFilter, null, indexer, sonarLintCache, analysisWarnings); + this(fileLinesContextFactory, checkFactory, noSonarFilter, null, indexer, sonarLintCache, analysisWarnings, new SensorTelemetryStorage()); } public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter, @Nullable PythonCustomRuleRepository[] customRuleRepositories, @Nullable PythonIndexer indexer, - @Nullable SonarLintCache sonarLintCache, AnalysisWarningsWrapper analysisWarnings) { + @Nullable SonarLintCache sonarLintCache, AnalysisWarningsWrapper analysisWarnings, SensorTelemetryStorage sensorTelemetryStorage) { this.checks = new PythonChecks(checkFactory) .addChecks(CheckList.REPOSITORY_KEY, CheckList.getChecks()) .addCustomChecks(customRuleRepositories); @@ -100,6 +97,12 @@ public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactor this.indexer = indexer; this.sonarLintCache = sonarLintCache; this.analysisWarnings = analysisWarnings; + this.sensorTelemetryStorage = sensorTelemetryStorage; + } + + public PythonSensor(FileLinesContextFactory fileLinesContextFactory, CheckFactory checkFactory, NoSonarFilter noSonarFilter, PythonCustomRuleRepository[] customRuleRepositories, + AnalysisWarningsWrapper analysisWarnings, SensorTelemetryStorage sensorTelemetryStorage) { + this(fileLinesContextFactory, checkFactory, noSonarFilter, customRuleRepositories, null, null, analysisWarnings, sensorTelemetryStorage); } @Override @@ -121,15 +124,25 @@ public void execute(SensorContext context) { if (pythonVersionParameter.length != 0){ ProjectPythonVersion.setCurrentVersions(PythonVersionUtils.fromStringArray(pythonVersionParameter)); } + setTelemetry(context, pythonVersionParameter); CacheContext cacheContext = CacheContextImpl.of(context); PythonIndexer pythonIndexer = this.indexer != null ? this.indexer : new SonarQubePythonIndexer(pythonFiles, cacheContext, context); pythonIndexer.setSonarLintCache(sonarLintCache); TypeShed.setProjectLevelSymbolTable(pythonIndexer.projectLevelSymbolTable()); PythonScanner scanner = new PythonScanner(context, checks, fileLinesContextFactory, noSonarFilter, PythonParser.create(), pythonIndexer); scanner.execute(pythonFiles, context); + sensorTelemetryStorage.send(context); durationReport.stop(); } + private void setTelemetry(SensorContext context, String[] pythonVersionParameter) { + var isVersionSet = pythonVersionParameter.length != 0 || context.runtime().getProduct() == SonarProduct.SONARLINT; + if (pythonVersionParameter.length != 0) { + sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_VERSION_KEY, String.join(",", pythonVersionParameter)); + } + sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_VERSION_SET_KEY, isVersionSet ? "1" : "0"); + } + private static List getInputFiles(SensorContext context) { FilePredicates p = context.fileSystem().predicates(); Iterable it = context.fileSystem().inputFiles(p.and(p.hasLanguage(Python.KEY))); diff --git a/sonar-python-plugin/src/main/java/org/sonar/plugins/python/TelemetryMetricKey.java b/sonar-python-plugin/src/main/java/org/sonar/plugins/python/TelemetryMetricKey.java index 65c8db782e..443b8f2227 100644 --- a/sonar-python-plugin/src/main/java/org/sonar/plugins/python/TelemetryMetricKey.java +++ b/sonar-python-plugin/src/main/java/org/sonar/plugins/python/TelemetryMetricKey.java @@ -20,7 +20,9 @@ public enum TelemetryMetricKey { NOTEBOOK_PRESENT_KEY("python.notebook.present"), NOTEBOOK_TOTAL_KEY("python.notebook.total"), NOTEBOOK_RECOGNITION_ERROR_KEY("python.notebook.recognition_error"), - NOTEBOOK_EXCEPTION_KEY("python.notebook.exceptions"); + NOTEBOOK_EXCEPTION_KEY("python.notebook.exceptions"), + PYTHON_VERSION_SET_KEY("python.version.set"), + PYTHON_VERSION_KEY("python.version"); private final String key; diff --git a/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonSensorTest.java b/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonSensorTest.java index 06406c22af..c9c0bb2fcf 100644 --- a/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonSensorTest.java +++ b/sonar-python-plugin/src/test/java/org/sonar/plugins/python/PythonSensorTest.java @@ -30,6 +30,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import javax.annotation.Nullable; @@ -1408,6 +1409,40 @@ void test_scanner_isNotebook() { assertThat(PythonScanner.isNotebook(notebookPythonFile)).isTrue(); } + @Test + void send_telemetry_with_version() { + activeRules = new ActiveRulesBuilder() + .addRule(new NewActiveRule.Builder() + .setRuleKey(RuleKey.of(CheckList.REPOSITORY_KEY, "S930")) + .build()) + .build(); + + context.setSettings(new MapSettings().setProperty("sonar.python.version", "3.10,3.13")); + + SensorTelemetryStorage sensorTelemetryStorage = spy(new SensorTelemetryStorage()); + PythonSensor sensor = sensor(sensorTelemetryStorage); + sensor.execute(context); + verify(sensorTelemetryStorage, times(1)).send(context); + assertThat(sensorTelemetryStorage.data()).containsExactlyInAnyOrderEntriesOf(Map.of(TelemetryMetricKey.PYTHON_VERSION_KEY, "3.10,3.13", + TelemetryMetricKey.PYTHON_VERSION_SET_KEY, "1")); + } + + @Test + void send_telemetry_no_version() { + activeRules = new ActiveRulesBuilder() + .addRule(new NewActiveRule.Builder() + .setRuleKey(RuleKey.of(CheckList.REPOSITORY_KEY, "S930")) + .build()) + .build(); + + SensorTelemetryStorage sensorTelemetryStorage = spy(new SensorTelemetryStorage()); + PythonSensor sensor = sensor(sensorTelemetryStorage); + sensor.execute(context); + verify(sensorTelemetryStorage, times(1)).send(context); + assertThat(sensorTelemetryStorage.data()).containsExactlyInAnyOrderEntriesOf(Map.of(TelemetryMetricKey.PYTHON_VERSION_SET_KEY, "0")); + + } + private com.sonar.sslr.api.Token passToken(URI uri) { return com.sonar.sslr.api.Token.builder() .setType(PythonKeyword.PASS) @@ -1422,7 +1457,16 @@ private PythonSensor sensor() { return sensor(CUSTOM_RULES, null, analysisWarning); } + private PythonSensor sensor(SensorTelemetryStorage sensorTelemetryStorage) { + return sensor(CUSTOM_RULES, null, analysisWarning, sensorTelemetryStorage); + } + private PythonSensor sensor(@Nullable PythonCustomRuleRepository[] customRuleRepositories, @Nullable PythonIndexer indexer, AnalysisWarningsWrapper analysisWarnings) { + return sensor(customRuleRepositories, indexer, analysisWarnings, new SensorTelemetryStorage()); + } + + private PythonSensor sensor(@Nullable PythonCustomRuleRepository[] customRuleRepositories, @Nullable PythonIndexer indexer, AnalysisWarningsWrapper analysisWarnings, + SensorTelemetryStorage sensorTelemetryStorage) { FileLinesContextFactory fileLinesContextFactory = mock(FileLinesContextFactory.class); FileLinesContext fileLinesContext = mock(FileLinesContext.class); when(fileLinesContextFactory.createFor(Mockito.any(InputFile.class))).thenReturn(fileLinesContext); @@ -1431,12 +1475,13 @@ private PythonSensor sensor(@Nullable PythonCustomRuleRepository[] customRuleRep return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), analysisWarnings); } if (indexer == null) { - return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), customRuleRepositories, analysisWarnings); + return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), customRuleRepositories, analysisWarnings, sensorTelemetryStorage); } if (customRuleRepositories == null) { return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), indexer, new SonarLintCache(), analysisWarnings); } - return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), customRuleRepositories, indexer, new SonarLintCache(), analysisWarnings); + return new PythonSensor(fileLinesContextFactory, checkFactory, mock(NoSonarFilter.class), customRuleRepositories, indexer, new SonarLintCache(), analysisWarnings, + sensorTelemetryStorage); } private SonarLintPythonIndexer pythonIndexer(List files) {