Skip to content

Commit 759193c

Browse files
thomas-serre-sonarsourcejoke1196
authored andcommitted
SONARPY-3241: Add import telemetry metrics (#862)
Co-authored-by: joke1196 <david.kunzmann@sonarsource.com> GitOrigin-RevId: f42b590fc398082eea83889e3c887a6ceaa3b594
1 parent 24e51d2 commit 759193c

12 files changed

Lines changed: 1092 additions & 99 deletions

python-commons/src/main/java/org/sonar/plugins/python/PythonSensor.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.sonar.plugins.python.editions.RepositoryInfoProvider.RepositoryInfo;
4848
import org.sonar.plugins.python.editions.RepositoryInfoProviderWrapper;
4949
import org.sonar.plugins.python.indexer.NamespacePackageTelemetry;
50+
import org.sonar.plugins.python.indexer.PackageResolutionResult;
5051
import org.sonar.plugins.python.indexer.PythonIndexer;
5152
import org.sonar.plugins.python.indexer.PythonIndexerWrapper;
5253
import org.sonar.plugins.python.indexer.SonarQubePythonIndexer;
@@ -226,7 +227,53 @@ private void updateNamespacePackageTelemetry(PythonIndexer pythonIndexer) {
226227
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGES_WITHOUT_INIT, telemetry.packagesWithoutInit());
227228
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_DUPLICATE_PACKAGES_WITHOUT_INIT, telemetry.duplicatePackagesWithoutInit());
228229
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_NAMESPACE_PACKAGES_IN_REGULAR_PACKAGE, telemetry.namespacePackagesInRegularPackage());
230+
231+
updateResolutionMethodTelemetry(telemetry);
232+
updateBuildSystemTelemetry(telemetry);
233+
}
234+
}
235+
236+
private void updateResolutionMethodTelemetry(NamespacePackageTelemetry telemetry) {
237+
PackageResolutionResult.ResolutionMethod method = telemetry.resolutionMethod();
238+
if (method == null) {
239+
return;
240+
}
241+
242+
// Report boolean flags for each resolution method
243+
boolean usedPyproject = method == PackageResolutionResult.ResolutionMethod.PYPROJECT_TOML
244+
|| method == PackageResolutionResult.ResolutionMethod.PYPROJECT_AND_SETUP_PY;
245+
boolean usedSetupPy = method == PackageResolutionResult.ResolutionMethod.SETUP_PY
246+
|| method == PackageResolutionResult.ResolutionMethod.PYPROJECT_AND_SETUP_PY;
247+
boolean usedSonarSources = method == PackageResolutionResult.ResolutionMethod.SONAR_SOURCES;
248+
boolean usedConventionalFolders = method == PackageResolutionResult.ResolutionMethod.CONVENTIONAL_FOLDERS;
249+
boolean usedBaseDir = method == PackageResolutionResult.ResolutionMethod.BASE_DIR;
250+
251+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_RESOLVED_VIA_PYPROJECT_TOML, usedPyproject ? 1 : 0);
252+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_RESOLVED_VIA_SETUP_PY, usedSetupPy ? 1 : 0);
253+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_RESOLVED_VIA_SONAR_SOURCES, usedSonarSources ? 1 : 0);
254+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_RESOLVED_VIA_CONVENTIONAL_FOLDERS, usedConventionalFolders ? 1 : 0);
255+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_RESOLVED_VIA_BASE_DIR, usedBaseDir ? 1 : 0);
256+
}
257+
258+
private void updateBuildSystemTelemetry(NamespacePackageTelemetry telemetry) {
259+
PackageResolutionResult.BuildSystem buildSystem = telemetry.buildSystem();
260+
if (buildSystem == null) {
261+
return;
229262
}
263+
264+
// Report boolean flags for each build system
265+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_SETUPTOOLS,
266+
buildSystem == PackageResolutionResult.BuildSystem.SETUPTOOLS ? 1 : 0);
267+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_POETRY,
268+
buildSystem == PackageResolutionResult.BuildSystem.POETRY ? 1 : 0);
269+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_HATCHLING,
270+
buildSystem == PackageResolutionResult.BuildSystem.HATCHLING ? 1 : 0);
271+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_UV_BUILD,
272+
buildSystem == PackageResolutionResult.BuildSystem.UV_BUILD ? 1 : 0);
273+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_PDM,
274+
buildSystem == PackageResolutionResult.BuildSystem.PDM ? 1 : 0);
275+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_PACKAGE_BUILD_SYSTEM_FLIT,
276+
buildSystem == PackageResolutionResult.BuildSystem.FLIT ? 1 : 0);
230277
}
231278

232279
private void updateSonarTestsTelemetry(SensorContext context) {

python-commons/src/main/java/org/sonar/plugins/python/indexer/NamespacePackageAnalyzer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ public NamespacePackageTelemetry analyze(ProjectTree projectTree) {
5757
packagesWithInit,
5858
packagesWithoutInit,
5959
duplicatePackagesWithoutInit,
60-
namespacePackagesInRegularPackage);
60+
namespacePackagesInRegularPackage,
61+
null,
62+
null);
6163
}
6264

6365
private static boolean hasAnyParentWithInit(ProjectTree.ProjectTreeFolder folder) {

python-commons/src/main/java/org/sonar/plugins/python/indexer/NamespacePackageTelemetry.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,43 @@
1616
*/
1717
package org.sonar.plugins.python.indexer;
1818

19+
import javax.annotation.Nullable;
20+
1921
/**
2022
* Telemetry data for namespace packages.
2123
*
2224
* @param packagesWithInit The number of packages with an __init__.py file.
2325
* @param packagesWithoutInit The number of packages without an __init__.py file.
2426
* @param duplicatePackagesWithoutInit The number of packages without an __init__.py file that appear multiple times. Each occurrence increments this count.
2527
* @param namespacePackagesInRegularPackage The number of packages without an __init__.py file that have at least one parent with an __init__.py file.
28+
* @param resolutionMethod How the package roots were resolved (e.g., pyproject.toml, setup.py, sonar.sources, fallback).
29+
* @param buildSystem The build system identified when resolution method is PYPROJECT_TOML (e.g., setuptools, poetry).
2630
*/
2731
public record NamespacePackageTelemetry(
2832
int packagesWithInit,
2933
int packagesWithoutInit,
3034
int duplicatePackagesWithoutInit,
31-
int namespacePackagesInRegularPackage) {
35+
int namespacePackagesInRegularPackage,
36+
@Nullable PackageResolutionResult.ResolutionMethod resolutionMethod,
37+
@Nullable PackageResolutionResult.BuildSystem buildSystem) {
3238

3339
public static NamespacePackageTelemetry empty() {
34-
return new NamespacePackageTelemetry(0, 0, 0, 0);
40+
return new NamespacePackageTelemetry(0, 0, 0, 0, null, null);
41+
}
42+
43+
/**
44+
* Creates a new telemetry instance with resolution information added.
45+
*/
46+
public NamespacePackageTelemetry withResolutionInfo(
47+
PackageResolutionResult.ResolutionMethod method,
48+
PackageResolutionResult.BuildSystem system) {
49+
return new NamespacePackageTelemetry(
50+
packagesWithInit,
51+
packagesWithoutInit,
52+
duplicatePackagesWithoutInit,
53+
namespacePackagesInRegularPackage,
54+
method,
55+
system);
3556
}
3657
}
3758

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python.indexer;
18+
19+
import java.util.List;
20+
21+
/**
22+
* Result of package root resolution containing both the resolved roots and
23+
* information about how they were resolved.
24+
*
25+
* @param roots The resolved package root absolute paths
26+
* @param method The method used to resolve the package roots
27+
* @param buildSystem The build system identified (only applicable for PYPROJECT_TOML method)
28+
*/
29+
public record PackageResolutionResult(
30+
List<String> roots,
31+
ResolutionMethod method,
32+
BuildSystem buildSystem) {
33+
34+
/**
35+
* How the package roots were resolved.
36+
*/
37+
public enum ResolutionMethod {
38+
/** Resolved from pyproject.toml build configuration */
39+
PYPROJECT_TOML,
40+
/** Resolved from setup.py configuration */
41+
SETUP_PY,
42+
/** Resolved from both pyproject.toml and setup.py */
43+
PYPROJECT_AND_SETUP_PY,
44+
/** Resolved from sonar.sources property */
45+
SONAR_SOURCES,
46+
/** Resolved from conventional folders (src/, lib/) */
47+
CONVENTIONAL_FOLDERS,
48+
/** Fallback to project base directory */
49+
BASE_DIR
50+
}
51+
52+
/**
53+
* Build systems supported in pyproject.toml.
54+
*/
55+
public enum BuildSystem {
56+
SETUPTOOLS,
57+
POETRY,
58+
HATCHLING,
59+
UV_BUILD,
60+
UV_BUILD_DEFAULT_MODULE,
61+
PDM,
62+
FLIT,
63+
/** Multiple build systems detected */
64+
MULTIPLE,
65+
/** Used when resolution method is not PYPROJECT_TOML */
66+
NONE
67+
}
68+
69+
public static PackageResolutionResult fromPyProjectToml(List<String> roots, BuildSystem buildSystem) {
70+
return new PackageResolutionResult(roots, ResolutionMethod.PYPROJECT_TOML, buildSystem);
71+
}
72+
73+
public static PackageResolutionResult fromSetupPy(List<String> roots) {
74+
return new PackageResolutionResult(roots, ResolutionMethod.SETUP_PY, BuildSystem.NONE);
75+
}
76+
77+
public static PackageResolutionResult fromBothPyProjectAndSetupPy(List<String> roots, BuildSystem buildSystem) {
78+
return new PackageResolutionResult(roots, ResolutionMethod.PYPROJECT_AND_SETUP_PY, buildSystem);
79+
}
80+
81+
public static PackageResolutionResult fromSonarSources(List<String> roots) {
82+
return new PackageResolutionResult(roots, ResolutionMethod.SONAR_SOURCES, BuildSystem.NONE);
83+
}
84+
85+
public static PackageResolutionResult fromConventionalFolders(List<String> roots) {
86+
return new PackageResolutionResult(roots, ResolutionMethod.CONVENTIONAL_FOLDERS, BuildSystem.NONE);
87+
}
88+
89+
public static PackageResolutionResult fromBaseDir(List<String> roots) {
90+
return new PackageResolutionResult(roots, ResolutionMethod.BASE_DIR, BuildSystem.NONE);
91+
}
92+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python.indexer;
18+
19+
import java.io.File;
20+
import java.util.List;
21+
22+
/**
23+
* Result of extracting source roots from pyproject.toml, including the build system detected.
24+
*
25+
* @param configRoots The extracted source roots
26+
* @param buildSystem The build system that provided the source roots
27+
*/
28+
public record PyProjectExtractionResult(ConfigSourceRoots configRoots, PackageResolutionResult.BuildSystem buildSystem){
29+
30+
public static PyProjectExtractionResult empty(File file) {
31+
return new PyProjectExtractionResult(ConfigSourceRoots.empty(file), PackageResolutionResult.BuildSystem.NONE);
32+
}
33+
34+
public boolean hasRoots() {
35+
return !configRoots.relativeRoots().isEmpty();
36+
}
37+
38+
public List<String> relativeRoots(){
39+
return configRoots.relativeRoots();
40+
}
41+
}

0 commit comments

Comments
 (0)