3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.telemetry;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.io.StringWriter;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Locale;
31 import java.util.Random;
33 import java.util.stream.Collectors;
34 import java.util.stream.IntStream;
35 import org.jetbrains.annotations.NotNull;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.sonar.api.utils.System2;
39 import org.sonar.api.utils.text.JsonWriter;
40 import org.sonar.core.platform.EditionProvider;
41 import org.sonar.core.telemetry.TelemetryExtension;
42 import org.sonar.db.project.CreationMethod;
43 import org.sonar.db.user.UserTelemetryDto;
44 import org.sonar.server.qualitygate.Condition;
45 import org.sonar.server.util.DigestUtil;
47 import static java.util.stream.Collectors.joining;
48 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
49 import static org.assertj.core.api.Assertions.assertThat;
50 import static org.mockito.Mockito.mock;
51 import static org.mockito.Mockito.when;
52 import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
53 import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
54 import static org.sonar.server.qualitygate.Condition.Operator.fromDbValue;
55 import static org.sonar.test.JsonAssert.assertJson;
57 @RunWith(DataProviderRunner.class)
58 public class TelemetryDataJsonWriterTest {
60 private final Random random = new Random();
62 private final TelemetryExtension extension = mock(TelemetryExtension.class);
64 private final System2 system2 = mock(System2.class);
66 private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
68 private static final int NCD_ID = 12345;
70 private static final TelemetryData.NewCodeDefinition NCD_INSTANCE = new TelemetryData.NewCodeDefinition(PREVIOUS_VERSION.name(), "", "instance");
71 private static final TelemetryData.NewCodeDefinition NCD_PROJECT = new TelemetryData.NewCodeDefinition(NUMBER_OF_DAYS.name(), "30", "project");
74 public void write_server_id_version_and_sequence() {
75 TelemetryData data = telemetryBuilder().build();
77 String json = writeTelemetryData(data);
78 assertJson(json).isSimilarTo("""
82 "messageSequenceNumber": %s
84 """.formatted(data.getServerId(), data.getVersion(), data.getMessageSequenceNumber()));
88 public void does_not_write_edition_if_null() {
89 TelemetryData data = telemetryBuilder().build();
91 String json = writeTelemetryData(data);
93 assertThat(json).doesNotContain("edition");
97 @UseDataProvider("allEditions")
98 public void writes_edition_if_non_null(EditionProvider.Edition edition) {
99 TelemetryData data = telemetryBuilder()
103 String json = writeTelemetryData(data);
104 assertJson(json).isSimilarTo("""
108 """.formatted(edition.name().toLowerCase(Locale.ENGLISH)));
112 public void writes_default_qg() {
113 TelemetryData data = telemetryBuilder()
114 .setDefaultQualityGate("default-qg")
117 String json = writeTelemetryData(data);
118 assertJson(json).isSimilarTo("""
120 "defaultQualityGate": "%s"
122 """.formatted(data.getDefaultQualityGate()));
126 public void writes_sonarWay_qg() {
127 TelemetryData data = telemetryBuilder()
128 .setSonarWayQualityGate("sonarWayUUID")
131 String json = writeTelemetryData(data);
132 assertJson(json).isSimilarTo("""
134 "sonarway_quality_gate_uuid": "%s"
136 """.formatted(data.getSonarWayQualityGate()));
140 public void writes_database() {
141 String name = randomAlphabetic(12);
142 String version = randomAlphabetic(10);
143 TelemetryData data = telemetryBuilder()
144 .setDatabase(new TelemetryData.Database(name, version))
147 String json = writeTelemetryData(data);
148 assertJson(json).isSimilarTo("""
155 """.formatted(name, version));
159 public void writes_no_plugins() {
160 TelemetryData data = telemetryBuilder()
161 .setPlugins(Collections.emptyMap())
164 String json = writeTelemetryData(data);
166 assertJson(json).isSimilarTo("""
174 public void writes_all_plugins() {
175 Map<String, String> plugins = IntStream.range(0, 1 + random.nextInt(10))
177 .collect(Collectors.toMap(i -> "P" + i, i1 -> "V" + i1));
178 TelemetryData data = telemetryBuilder()
182 String json = writeTelemetryData(data);
183 assertJson(json).isSimilarTo("""
187 """.formatted(plugins.entrySet().stream().map(e -> "{\"name\":\"" + e.getKey() + "\",\"version\":\"" + e.getValue() + "\"}").collect(joining(","))));
191 public void does_not_write_installation_date_if_null() {
192 TelemetryData data = telemetryBuilder()
193 .setInstallationDate(null)
196 String json = writeTelemetryData(data);
198 assertThat(json).doesNotContain("installationDate");
202 public void write_installation_date_in_utc_format() {
203 TelemetryData data = telemetryBuilder()
204 .setInstallationDate(1_000L)
207 String json = writeTelemetryData(data);
209 assertJson(json).isSimilarTo("""
211 "installationDate":"1970-01-01T00:00:01+0000"
217 public void does_not_write_installation_version_if_null() {
218 TelemetryData data = telemetryBuilder()
219 .setInstallationVersion(null)
222 String json = writeTelemetryData(data);
224 assertThat(json).doesNotContain("installationVersion");
228 public void write_installation_version() {
229 String installationVersion = randomAlphabetic(5);
230 TelemetryData data = telemetryBuilder()
231 .setInstallationVersion(installationVersion)
234 String json = writeTelemetryData(data);
235 assertJson(json).isSimilarTo("""
237 "installationVersion": "%s"
239 """.formatted(installationVersion));
243 @UseDataProvider("getFeatureFlagEnabledStates")
244 public void write_container_flag(boolean isIncontainer) {
245 TelemetryData data = telemetryBuilder()
246 .setInContainer(isIncontainer)
249 String json = writeTelemetryData(data);
250 assertJson(json).isSimilarTo("""
254 """.formatted(isIncontainer));
258 public static Object[][] getManagedInstanceData() {
259 return new Object[][] {
268 @UseDataProvider("getManagedInstanceData")
269 public void writeTelemetryData_encodesCorrectlyManagedInstanceInformation(boolean isManaged, String provider) {
270 TelemetryData data = telemetryBuilder()
271 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(isManaged, provider))
274 String json = writeTelemetryData(data);
277 assertJson(json).isSimilarTo("""
279 "managedInstanceInformation": {
284 """.formatted(provider));
286 assertJson(json).isSimilarTo("""
288 "managedInstanceInformation": {
297 public void writeTelemetryData_shouldWriteCloudUsage() {
298 TelemetryData data = telemetryBuilder().build();
300 String json = writeTelemetryData(data);
301 assertJson(json).isSimilarTo("""
305 "kubernetesVersion": "1.27",
306 "kubernetesPlatform": "linux/amd64",
307 "kubernetesProvider": "5.4.181-99.354.amzn2.x86_64",
308 "officialHelmChart": "10.1.0",
309 "officialImage": false,
310 "containerRuntime": "docker"
317 public void writes_has_unanalyzed_languages() {
318 TelemetryData data = telemetryBuilder()
319 .setHasUnanalyzedC(true)
320 .setHasUnanalyzedCpp(false)
323 String json = writeTelemetryData(data);
325 assertJson(json).isSimilarTo("""
327 "hasUnanalyzedC": true,
328 "hasUnanalyzedCpp": false,
334 public void writes_security_custom_config() {
335 TelemetryData data = telemetryBuilder()
336 .setCustomSecurityConfigs(Set.of("php", "java"))
339 String json = writeTelemetryData(data);
341 assertJson(json).isSimilarTo("""
343 "customSecurityConfig": ["php", "java"]
349 public void writes_local_timestamp() {
350 when(system2.now()).thenReturn(1000L);
352 TelemetryData data = telemetryBuilder().build();
353 String json = writeTelemetryData(data);
355 assertJson(json).isSimilarTo("""
357 "localTimestamp": "1970-01-01T00:00:01+0000"
363 public void writes_all_users_with_anonymous_md5_uuids() {
364 TelemetryData data = telemetryBuilder()
365 .setUsers(attachUsers())
368 String json = writeTelemetryData(data);
370 assertJson(json).isSimilarTo("""
376 "identityProvider": "gitlab",
377 "lastActivity": "1970-01-01T00:00:00+0000",
378 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
383 "status": "inactive",
384 "identityProvider": "gitlab",
385 "lastActivity": "1970-01-01T00:00:00+0000",
386 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
392 "identityProvider": "gitlab",
393 "lastActivity": "1970-01-01T00:00:00+0000",
394 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
400 .formatted(DigestUtil.sha3_224Hex("uuid-0"), DigestUtil.sha3_224Hex("uuid-1"), DigestUtil.sha3_224Hex("uuid-2")));
404 public void writes_all_projects() {
405 TelemetryData data = telemetryBuilder()
406 .setProjects(attachProjects())
409 String json = writeTelemetryData(data);
411 assertJson(json).isSimilarTo("""
415 "projectUuid": "uuid-0",
416 "lastAnalysis": "1970-01-01T00:00:00+0000",
417 "language": "lang-0",
418 "qualityProfile" : "qprofile-0",
422 "projectUuid": "uuid-1",
423 "lastAnalysis": "1970-01-01T00:00:00+0000",
424 "language": "lang-1",
425 "qualityProfile" : "qprofile-1",
429 "projectUuid": "uuid-2",
430 "lastAnalysis": "1970-01-01T00:00:00+0000",
431 "language": "lang-2",
432 "qualityProfile" : "qprofile-2",
441 public void writeTelemetryData_whenAnalyzedLanguages_shouldwriteAllProjectsStats() {
442 TelemetryData data = telemetryBuilder()
443 .setProjectStatistics(attachProjectStatsWithMetrics())
446 String json = writeTelemetryData(data);
448 assertJson(json).isSimilarTo("""
450 "projects-general-stats": [
452 "projectUuid": "uuid-0",
454 "pullRequestCount": 2,
455 "qualityGate": "qg-0",
458 "devopsPlatform": "devops-0",
460 "vulnerabilities": 3,
461 "securityHotspots": 4,
463 "developmentCost": 30,
465 "externalSecurityReportExportedAt": 1500000,
466 "project_creation_method": "LOCAL_API",
470 "projectUuid": "uuid-1",
472 "pullRequestCount": 4,
473 "qualityGate": "qg-1",
476 "devopsPlatform": "devops-1",
478 "vulnerabilities": 6,
479 "securityHotspots": 8,
480 "technicalDebt": 120,
481 "developmentCost": 60,
483 "externalSecurityReportExportedAt": 1500001,
484 "project_creation_method": "LOCAL_API",
488 "projectUuid": "uuid-2",
490 "pullRequestCount": 6,
491 "qualityGate": "qg-2",
494 "devopsPlatform": "devops-2",
496 "vulnerabilities": 9,
497 "securityHotspots": 12,
498 "technicalDebt": 180,
499 "developmentCost": 90,
501 "externalSecurityReportExportedAt": 1500002,
502 "project_creation_method": "LOCAL_API",
511 public void writes_all_projects_stats_with_unanalyzed_languages() {
512 TelemetryData data = telemetryBuilder()
513 .setProjectStatistics(attachProjectStats())
516 String json = writeTelemetryData(data);
517 assertThat(json).doesNotContain("hasUnanalyzedC", "hasUnanalyzedCpp");
521 public void writes_all_projects_stats_without_missing_metrics() {
522 TelemetryData data = telemetryBuilder()
523 .setProjectStatistics(attachProjectStats())
525 String json = writeTelemetryData(data);
526 assertThat(json).doesNotContain("bugs", "vulnerabilities", "securityHotspots", "technicalDebt", "developmentCost");
530 public void writes_all_quality_gates() {
531 TelemetryData data = telemetryBuilder()
532 .setQualityGates(attachQualityGates())
535 String json = writeTelemetryData(data);
536 assertJson(json).isSimilarTo("""
541 "caycStatus": "non-compliant",
544 "metric": "new_coverage",
545 "comparison_operator": "LT",
549 "metric": "new_duplicated_lines_density",
550 "comparison_operator": "GT",
557 "caycStatus": "compliant",
560 "metric": "new_coverage",
561 "comparison_operator": "LT",
565 "metric": "new_duplicated_lines_density",
566 "comparison_operator": "GT",
573 "caycStatus": "over-compliant",
576 "metric": "new_coverage",
577 "comparison_operator": "LT",
581 "metric": "new_duplicated_lines_density",
582 "comparison_operator": "GT",
593 public void writeTelemetryData_shouldWriteQualityProfiles() {
594 TelemetryData data = telemetryBuilder()
595 .setQualityProfiles(List.of(
596 new TelemetryData.QualityProfile("uuid-1", "parent-uuid-1", "js", true, false, true, 2, 3, 4),
597 new TelemetryData.QualityProfile("uuid-1", null, "js", false, true, null, null, null, null)))
600 String json = writeTelemetryData(data);
601 assertJson(json).isSimilarTo("""
603 "quality-profiles": [
606 "parentUuid": "parent-uuid-1",
610 "builtInParent": true,
611 "rulesOverriddenCount": 2,
612 "rulesActivatedCount": 3,
613 "rulesDeactivatedCount": 4
626 public void writes_all_branches() {
627 TelemetryData data = telemetryBuilder()
628 .setBranches(attachBranches())
631 String json = writeTelemetryData(data);
632 assertJson(json).isSimilarTo("""
636 "projectUuid": "projectUuid1",
637 "branchUuid": "branchUuid1",
639 "greenQualityGateCount": 1,
641 "excludeFromPurge": true
644 "projectUuid": "projectUuid2",
645 "branchUuid": "branchUuid2",
647 "greenQualityGateCount": 0,
649 "excludeFromPurge": true
657 public void writes_new_code_definitions() {
658 TelemetryData data = telemetryBuilder()
659 .setNewCodeDefinitions(attachNewCodeDefinitions())
662 String json = writeTelemetryData(data);
663 assertJson(json).isSimilarTo("""
665 "new-code-definitions": [
681 """.formatted(NCD_INSTANCE.hashCode(), NCD_INSTANCE.type(), NCD_INSTANCE.value(), NCD_INSTANCE.scope(), NCD_PROJECT.hashCode(),
682 NCD_PROJECT.type(), NCD_PROJECT.value(), NCD_PROJECT.scope()));
686 public void writes_instance_new_code_definition() {
687 TelemetryData data = telemetryBuilder().build();
689 String json = writeTelemetryData(data);
690 assertThat(json).contains("ncdId");
694 private static TelemetryData.Builder telemetryBuilder() {
695 return TelemetryData.builder()
698 .setMessageSequenceNumber(1L)
699 .setPlugins(Collections.emptyMap())
700 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(false, null))
701 .setCloudUsage(new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", "docker", false))
702 .setDatabase(new TelemetryData.Database("H2", "11"))
707 private static List<UserTelemetryDto> attachUsers() {
708 return IntStream.range(0, 3)
710 i -> new UserTelemetryDto().setUuid("uuid-" + i).setActive(i % 2 == 0).setLastConnectionDate(1L)
711 .setLastSonarlintConnectionDate(2L).setExternalIdentityProvider("gitlab").setScimUuid(i % 2 == 0 ? "scim-uuid-" + i : null))
715 private static List<TelemetryData.Project> attachProjects() {
716 return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.Project("uuid-" + i, 1L, "lang-" + i, "qprofile-" + i, (i + 1L) * 2)).toList();
719 private static List<TelemetryData.ProjectStatistics> attachProjectStatsWithMetrics() {
720 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsWithMetricBuilder(i).build()).toList();
723 private static List<TelemetryData.ProjectStatistics> attachProjectStats() {
724 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsBuilder(i).build()).toList();
727 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsBuilder(int i) {
728 return new TelemetryData.ProjectStatistics.Builder()
729 .setProjectUuid("uuid-" + i)
730 .setBranchCount((i + 1L) * 2L)
731 .setPRCount((i + 1L) * 2L)
732 .setQG("qg-" + i).setCi("ci-" + i)
734 .setDevops("devops-" + i)
736 .setCreationMethod(CreationMethod.LOCAL_API)
740 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsWithMetricBuilder(int i) {
741 return getProjectStatisticsBuilder(i)
742 .setBugs((i + 1L) * 2)
743 .setVulnerabilities((i + 1L) * 3)
744 .setSecurityHotspots((i + 1L) * 4)
745 .setDevelopmentCost((i + 1L) * 30d)
746 .setTechnicalDebt((i + 1L) * 60d)
747 .setExternalSecurityReportExportedAt(1_500_000L + i)
748 .setCreationMethod(CreationMethod.LOCAL_API)
749 .setMonorepo(i % 2 == 0);
752 private List<TelemetryData.QualityGate> attachQualityGates() {
753 List<Condition> qualityGateConditions = attachQualityGateConditions();
754 return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant", qualityGateConditions),
755 new TelemetryData.QualityGate("uuid-1", "compliant", qualityGateConditions),
756 new TelemetryData.QualityGate("uuid-2", "over-compliant", qualityGateConditions));
759 private List<Condition> attachQualityGateConditions() {
760 return List.of(new Condition("new_coverage", fromDbValue("LT"), "80"),
761 new Condition("new_duplicated_lines_density", fromDbValue("GT"), "3"));
764 private List<TelemetryData.Branch> attachBranches() {
765 return List.of(new TelemetryData.Branch("projectUuid1", "branchUuid1", NCD_ID, 1, 2, true),
766 new TelemetryData.Branch("projectUuid2", "branchUuid2", NCD_ID, 0, 2, true));
769 private List<TelemetryData.NewCodeDefinition> attachNewCodeDefinitions() {
770 return List.of(NCD_INSTANCE, NCD_PROJECT);
774 public static Object[][] allEditions() {
775 return Arrays.stream(EditionProvider.Edition.values())
776 .map(t -> new Object[] {t})
777 .toArray(Object[][]::new);
780 private String writeTelemetryData(TelemetryData data) {
781 StringWriter jsonString = new StringWriter();
782 try (JsonWriter json = JsonWriter.of(jsonString)) {
783 underTest.writeTelemetryData(json, data);
785 return jsonString.toString();
789 public static Set<Boolean> getFeatureFlagEnabledStates() {
790 return Set.of(true, false);