Skip to content

Commit 833cbe1

Browse files
martin-strecker-sonarsourceTim-Pohlmannclaude
authored
SCAN4NET-486 Support ServerSettings in telemetry (#3007)
Co-authored-by: Tim Pohlmann <tim.pohlmann@sonarsource.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 379f2c1 commit 833cbe1

File tree

7 files changed

+145
-15
lines changed

7 files changed

+145
-15
lines changed

Tests/SonarScanner.MSBuild.Common.Test/AggregatePropertiesProviderTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,14 @@ public void AggProperties_GetAllPropertiesPerProvider()
133133
});
134134
}
135135

136+
[TestMethod]
137+
public void AggProperties_NestedAggregate_ReturnsLeafProvider()
138+
{
139+
new AggregatePropertiesProvider(new ListPropertiesProvider(), new AggregatePropertiesProvider(new ListPropertiesProvider([new("key", "value")])))
140+
.TryGetProperty("key", out _, out var provider)
141+
.Should().BeTrue();
142+
provider.Should().BeOfType<ListPropertiesProvider>().Which.HasProperty("key");
143+
}
144+
136145
#endregion Tests
137146
}

Tests/SonarScanner.MSBuild.PreProcessor.Test/PreProcessorTests.Telemetry.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public async Task Execute_WritesTelemetry_SetViaMultipleSources_ProviderWithHigh
7777
{
7878
"/d:sonar.scanner.scanAll=false"
7979
};
80-
var telemetry = await CreateTelemetry(args, new KeyValuePair<string, string>("SONARQUBE_SCANNER_PARAMS", """{"sonar.scanner.scanAll": "true"}"""));
80+
var telemetry = await CreateTelemetry(args, null, new KeyValuePair<string, string>("SONARQUBE_SCANNER_PARAMS", """{"sonar.scanner.scanAll": "true"}"""));
8181

8282
// Note: cmd.line1 is an unknown property and is not reported (only whitelisted properties are reported)
8383
telemetry.Should().HaveMessage("dotnetenterprise.s4net.params.sonar_log_level.source", "CLI")
@@ -87,6 +87,37 @@ public async Task Execute_WritesTelemetry_SetViaMultipleSources_ProviderWithHigh
8787
.And.HaveMessage("dotnetenterprise.s4net.serverInfo.version", "5.6");
8888
}
8989

90+
[TestMethod]
91+
public async Task Execute_WritesTelemetry_ServerSettings()
92+
{
93+
var serverProperties = new Dictionary<string, string>
94+
{
95+
{ "sonar.cs.ignoreHeaderComments", "true" }, // default value
96+
{ "sonar.cs.analyzeGeneratedCode", "false" }, // default value
97+
{ "sonar.vbnet.ignoreHeaderComments", "false" }, // not default value
98+
{ "sonar.vbnet.analyzeGeneratedCode", "true" }, // not default value
99+
{ "sonar.cs.opencover.reportsPaths", "report.xml" }, // has no default
100+
{ "sonar.exclusions", "**/*.generated.cs" }, // has no default
101+
{ "not whitelisted", "value" }
102+
};
103+
var args = new List<string>(CreateArgs())
104+
{
105+
"/d:sonar.cs.analyzeGeneratedCode=false",
106+
"/d:sonar.vbnet.analyzeGeneratedCode=false",
107+
"/d:sonar.exclusions=**/*.generated.cs",
108+
};
109+
var telemetry = await CreateTelemetry(args, serverProperties);
110+
111+
telemetry.Should()
112+
.HaveMessage("dotnetenterprise.s4net.params.sonar_vbnet_ignoreheadercomments.source", "SQ_SERVER_SETTINGS")
113+
.And.HaveMessage("dotnetenterprise.s4net.params.sonar_cs_opencover_reportspaths.source", "SQ_SERVER_SETTINGS")
114+
.And.HaveMessage("dotnetenterprise.s4net.params.sonar_cs_analyzegeneratedcode.source", "CLI")
115+
.And.HaveMessage("dotnetenterprise.s4net.params.sonar_vbnet_analyzegeneratedcode.source", "CLI")
116+
.And.HaveMessage("dotnetenterprise.s4net.params.sonar_exclusions.source", "CLI")
117+
.And.NotHaveKey("dotnetenterprise.s4net.params.sonar_cs_ignoreheadercomments.source")
118+
.And.NotHaveKey("not whitelisted");
119+
}
120+
90121
private static string CreateAnalysisXml(string parentDir, Dictionary<string, string> properties = null)
91122
{
92123
Directory.Exists(parentDir).Should().BeTrue("Test setup error: expecting the parent directory to exist: {0}", parentDir);
@@ -110,9 +141,13 @@ private static string CreateAnalysisXml(string parentDir, Dictionary<string, str
110141
return fullPath;
111142
}
112143

113-
private async Task<TestTelemetry> CreateTelemetry(IEnumerable<string> args = null, params KeyValuePair<string, string>[] environmentVariables)
144+
private async Task<TestTelemetry> CreateTelemetry(IEnumerable<string> args = null, Dictionary<string, string> serverProperties = null, params KeyValuePair<string, string>[] environmentVariables)
114145
{
115146
using var context = new Context(TestContext);
147+
if (serverProperties is not null)
148+
{
149+
context.Factory.Server.DownloadProperties(null, null).ReturnsForAnyArgs(serverProperties);
150+
}
116151
using var env = new EnvironmentVariableScope();
117152
foreach (var envVariable in environmentVariables)
118153
{

its/src/test/java/com/sonar/it/scanner/msbuild/sonarqube/TelemetryTest.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import com.sonar.it.scanner.msbuild.utils.*;
2323
import com.sonar.orchestrator.build.BuildResult;
24+
import java.util.Arrays;
25+
import java.util.Collections;
2426
import org.assertj.core.api.AbstractListAssert;
2527
import org.assertj.core.api.ObjectAssert;
2628
import org.jetbrains.annotations.NotNull;
@@ -29,6 +31,7 @@
2931

3032
import java.nio.file.Paths;
3133
import java.util.List;
34+
import org.sonarqube.ws.client.settings.SetRequest;
3235

3336
import static com.sonar.it.scanner.msbuild.utils.SonarAssertions.assertThat;
3437

@@ -39,7 +42,35 @@ class TelemetryTest {
3942
@MSBuildMinVersion(16)
4043
@ServerMinVersion("2025.3")
4144
void telemetry_telemetryFiles_areCorrect_CS() {
42-
var result = runAnalysis("Telemetry");
45+
var context = AnalysisContext.forServer(Paths.get("Telemetry", "Telemetry").toString());
46+
context.orchestrator.getServer().provisionProject(context.projectKey, context.projectKey);
47+
var settings = TestUtils.newWsClient(context.orchestrator).settings();
48+
java.util.function.Supplier<SetRequest> request = () -> new SetRequest().setComponent(context.projectKey);
49+
// Configure custom server settings
50+
settings.set(request.get()
51+
.setKey("sonar.cs.analyzeGeneratedCode")
52+
.setValue("false") // same as default, gets overridden by cli parameter
53+
);
54+
settings.set(request.get()
55+
.setKey("sonar.cs.analyzeRazorCode")
56+
.setValue("false") // overrides default
57+
);
58+
settings.set(request.get()
59+
.setKey("sonar.cs.dotcover.reportsPaths") // gets overridden by cli parameter
60+
.setValues(Collections.singletonList("**/*.dotcover.*.html"))
61+
);
62+
settings.set(request.get()
63+
.setKey("sonar.cs.opencover.reportsPaths")
64+
.setValues(Arrays.asList("opencover1.xml", "opencover2.xml"))
65+
);
66+
// Configure CLI properties
67+
context.begin
68+
.setDebugLogs()
69+
.setProperty("sonar.cs.dotcover.reportsPaths", "dotCover.Output.html") // overrides server setting
70+
.setProperty("sonar.cs.analyzeGeneratedCode", "true"); // overrides server setting
71+
72+
var result = context.runAnalysis();
73+
assertThat(result.isSuccess()).isTrue();
4374
assertThatEndLogMetrics(result.end()).satisfiesExactlyInAnyOrder(
4475
x -> assertThat(x).matches("csharp\\.cs\\.language_version\\.csharp7(_3)?=3"),
4576
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.params.sonar_scanner_skipjreprovisioning.source=CLI"),
@@ -54,11 +85,15 @@ void telemetry_telemetryFiles_areCorrect_CS() {
5485
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.jre.bootstrapping=Disabled"),
5586
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.scannerEngine.bootstrapping=Enabled"),
5687
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.scannerEngine\\.download=CacheHit"),
88+
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.params.sonar_cs_analyzerazorcode.source=SQ_SERVER_SETTINGS"),
89+
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.params.sonar_cs_opencover_reportspaths.source=SQ_SERVER_SETTINGS"),
90+
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.params.sonar_cs_analyzegeneratedcode.source=CLI"),
91+
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.params.sonar_cs_dotcover_reportspaths.source=CLI"),
92+
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
5793
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.legacyTFS=NotCalled"),
5894
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.Sarif.V1_0_0_0.Valid=True"),
5995
// coverage_conversion=true is tested in CodeCoverageTest.whenRunningOnAzureDevops_coverageIsImported
6096
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.coverage_conversion=false"),
61-
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
6297
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.visual_studio_version="),
6398
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.msbuild_version="),
6499
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.netcore_sdk_version="),
@@ -98,10 +133,10 @@ void telemetry_telemetryFiles_areCorrect_VB() {
98133
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.jre.bootstrapping=Disabled"),
99134
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.scannerEngine.bootstrapping=Enabled"),
100135
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.scannerEngine\\.download=CacheHit"),
136+
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
101137
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.legacyTFS=NotCalled"),
102138
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.Sarif.V1_0_0_0.Valid=True"),
103139
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.coverage_conversion=false"),
104-
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
105140
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.visual_studio_version="),
106141
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.msbuild_version="),
107142
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.netcore_sdk_version="),
@@ -131,10 +166,10 @@ void telemetry_telemetryFiles_areCorrect_CSVB_Mixed() {
131166
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.jre.bootstrapping=Disabled"),
132167
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.scannerEngine.bootstrapping=Enabled"),
133168
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.scannerEngine\\.download=CacheHit"),
169+
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
134170
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.legacyTFS=NotCalled"),
135171
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.Sarif.V1_0_0_0.Valid=True"),
136172
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.coverage_conversion=false"),
137-
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
138173
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.visual_studio_version="),
139174
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.msbuild_version="),
140175
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.netcore_sdk_version="),
@@ -176,10 +211,10 @@ void telemetry_multiTargetFramework_tfmsAreCorrectlyRecorded() {
176211
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.jre.bootstrapping=Disabled"),
177212
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.scannerEngine.bootstrapping=Enabled"),
178213
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.scannerEngine\\.download=CacheHit"),
214+
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
179215
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.legacyTFS=NotCalled"),
180216
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.Sarif.V1_0_0_0.Valid=True"),
181217
x -> assertThat(x).isEqualTo("dotnetenterprise.s4net.endstep.coverage_conversion=false"),
182-
x -> assertThat(x).matches("dotnetenterprise\\.s4net\\.begin\\.runtime=(netframework|netcore)"),
183218
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.visual_studio_version="),
184219
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.msbuild_version="),
185220
x -> assertThat(x).startsWith("dotnetenterprise.s4net.build.netcore_sdk_version="),

src/SonarScanner.MSBuild.Common/AnalysisProperties/AggregatePropertiesProvider.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,18 @@ public bool TryGetProperty(string key, out Property property, out IAnalysisPrope
7272
{
7373
if (current.TryGetProperty(key, out property))
7474
{
75-
provider = current;
75+
provider = UnwrapNestedProvider(current, key);
7676
return true;
7777
}
7878
}
7979

8080
return false;
8181
}
8282

83+
private static IAnalysisPropertyProvider UnwrapNestedProvider(IAnalysisPropertyProvider provider, string key) =>
84+
provider is AggregatePropertiesProvider aggregate
85+
? UnwrapNestedProvider(aggregate.providers.First(x => x.HasProperty(key)), key)
86+
: provider;
87+
8388
#endregion IAnalysisPropertyProvider interface
8489
}

src/SonarScanner.MSBuild.Common/Telemetry/TelemetryUtils.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ public static class TelemetryUtils
2727
// See https://github.com/SonarSource/sonar-dotnet-enterprise/blob/master/sonar-dotnet-core/src/main/java/org/sonarsource/dotnet/shared/plugins/telemetryjson/TelemetryUtils.java
2828
private static readonly Regex SanitizeKeyRegex = new("[^a-zA-Z0-9]", RegexOptions.None, RegexConstants.DefaultTimeout);
2929

30+
// Sources:
31+
// - https://github.com/SonarSource/sonar-dotnet-enterprise/blob/master/sonar-csharp-core/src/main/java/org/sonarsource/csharp/core/CSharpPropertyDefinitions.java:
32+
// - https://github.com/SonarSource/sonar-dotnet-enterprise/blob/master/sonar-dotnet-core/src/main/java/org/sonarsource/dotnet/shared/plugins/AbstractPropertyDefinitions.java
33+
// - https://docs.sonarsource.com/sonarqube-server/10.8/analyzing-source-code/analysis-parameters#analysis-scope
34+
private static readonly Dictionary<string, string> ServerPropertyDefaults = new(StringComparer.OrdinalIgnoreCase)
35+
{
36+
// C# analyzer properties
37+
{ "sonar.cs.analyzeRazorCode", "true" },
38+
{ "sonar.cs.ignoreHeaderComments", "true" },
39+
{ "sonar.cs.analyzeGeneratedCode", "false" },
40+
41+
// VB.NET analyzer properties
42+
{ "sonar.vbnet.ignoreHeaderComments", "true" },
43+
{ "sonar.vbnet.analyzeGeneratedCode", "false" },
44+
45+
// Common properties
46+
{ "sonar.filesize.limit", "20" }
47+
};
48+
3049
public static string SanitizeKey(string key) =>
3150
SanitizeKeyRegex.Replace(key, "_");
3251

@@ -121,6 +140,14 @@ private static IEnumerable<KeyValuePair<string, string>> SelectManyTelemetryProp
121140
}
122141
else if (IsSourceOnlyWhitelisted(property))
123142
{
143+
// Skip server settings that match their default values - they don't provide useful telemetry
144+
if (provider.ProviderType == PropertyProviderKind.SQ_SERVER_SETTINGS
145+
&& ServerPropertyDefaults.TryGetValue(property.Id, out var defaultValue)
146+
&& string.Equals(value, defaultValue, StringComparison.OrdinalIgnoreCase))
147+
{
148+
return [];
149+
}
150+
124151
// Report source only, not the value
125152
// See https://docs.google.com/spreadsheets/d/1L682GZWwVw5xUZPaFbYlJYN1m9whBu-uo1S-ZkpPq9A for the full list of whitelisted properties
126153
return MessagePair(provider, property, null);
@@ -222,6 +249,25 @@ private static bool IsSourceOnlyWhitelisted(Property property)
222249
|| property.IsKey("sonar.scm.revision")
223250
|| property.IsKey("sonar.buildString")
224251
|| property.IsKey("sonar.scanner.javaOpts")
252+
// https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/test-coverage/dotnet-test-coverage
253+
// sonar.cs.vscoveragexml.reportsPath and sonar.cs.vstest.reportsPaths are already handled via directoryPath above
254+
|| property.IsKey("sonar.vbnet.vscoveragexml.reportsPath")
255+
|| property.IsKey("sonar.vbnet.vstest.reportsPaths")
256+
|| property.IsKey("sonar.cs.dotcover.reportsPaths")
257+
|| property.IsKey("sonar.vbnet.dotcover.reportsPaths")
258+
|| property.IsKey("sonar.cs.opencover.reportsPaths")
259+
|| property.IsKey("sonar.cs.ncover3.reportsPaths")
260+
|| property.IsKey("sonar.vbnet.ncover3.reportsPaths")
261+
|| property.IsKey("sonar.cs.nunit.reportsPaths")
262+
|| property.IsKey("sonar.vbnet.nunit.reportsPaths")
263+
|| property.IsKey("sonar.cs.xunit.reportsPaths")
264+
|| property.IsKey("sonar.vbnet.xunit.reportsPaths")
265+
// UI settings
266+
|| property.IsKey("sonar.cs.analyzeGeneratedCode")
267+
|| property.IsKey("sonar.vbnet.analyzeGeneratedCode")
268+
|| property.IsKey("sonar.cs.ignoreHeaderComments")
269+
|| property.IsKey("sonar.vbnet.ignoreHeaderComments")
270+
|| property.IsKey("sonar.cs.analyzeRazorCode")
225271
// https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/analysis-scope/narrowing-the-focus
226272
|| property.IsKey("sonar.exclusions")
227273
|| property.IsKey("sonar.test.exclusions")

src/SonarScanner.MSBuild.PreProcessor/PreProcessor.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ private async Task<ArgumentsAndRuleSets> FetchArgumentsAndRuleSets(ISonarWebServ
160160

161161
args.TryGetSetting(SonarProperties.ProjectBranch, out var projectBranch);
162162
argumentsAndRuleSets.ServerSettings = await server.DownloadProperties(args.ProjectKey, projectBranch);
163+
164+
// Use the aggregate of local and server properties when generating the analyzer configuration
165+
// See bug 699: https://github.com/SonarSource/sonar-scanner-msbuild/issues/699
166+
var serverProperties = new ListPropertiesProvider(argumentsAndRuleSets.ServerSettings, PropertyProviderKind.SQ_SERVER_SETTINGS);
167+
var allProperties = new AggregatePropertiesProvider(args.AggregateProperties, serverProperties);
168+
TelemetryUtils.AddTelemetry(runtime.Telemetry, allProperties);
169+
163170
var availableLanguages = await server.DownloadAllLanguages();
164171
var knownLanguages = Languages.Where(availableLanguages.Contains).ToList();
165172
if (knownLanguages.Count == 0)
@@ -188,11 +195,6 @@ private async Task<ArgumentsAndRuleSets> FetchArgumentsAndRuleSets(ISonarWebServ
188195
// It is null if the processing of server settings and active rules resulted in an empty ruleset
189196
var localCacheTempPath = args.SettingOrDefault(SonarProperties.PluginCacheDirectory, string.Empty);
190197

191-
// Use the aggregate of local and server properties when generating the analyzer configuration
192-
// See bug 699: https://github.com/SonarSource/sonar-scanner-msbuild/issues/699
193-
var serverProperties = new ListPropertiesProvider(argumentsAndRuleSets.ServerSettings);
194-
var allProperties = new AggregatePropertiesProvider(args.AggregateProperties, serverProperties);
195-
196198
var analyzerProvider = factory.CreateRoslynAnalyzerProvider(server, localCacheTempPath, settings, allProperties, rules, language);
197199
if (analyzerProvider.SetupAnalyzer() is { } analyzerSettings)
198200
{

src/SonarScanner.MSBuild.PreProcessor/ProcessedArgs.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,6 @@ public ProcessedArgs(
173173

174174
IsValid &= CheckOrganizationValidity();
175175
AggregateProperties = new AggregatePropertiesProvider(cmdLineProperties, globalFileProperties, ScannerEnvProperties);
176-
TelemetryUtils.AddTelemetry(runtime.Telemetry, AggregateProperties);
177-
178176
AggregateProperties.TryGetValue(SonarProperties.HostUrl, out var sonarHostUrl); // Used for SQ and may also be set to https://SonarCloud.io
179177
AggregateProperties.TryGetValue(SonarProperties.SonarcloudUrl, out var sonarcloudUrl);
180178
AggregateProperties.TryGetValue(SonarProperties.Region, out var region);

0 commit comments

Comments
 (0)