3 * Copyright (C) 2009-2023 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.apache.commons.codec.digest.DigestUtils;
36 import org.jetbrains.annotations.NotNull;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 import org.sonar.api.utils.System2;
40 import org.sonar.api.utils.text.JsonWriter;
41 import org.sonar.core.platform.EditionProvider;
42 import org.sonar.core.telemetry.TelemetryExtension;
43 import org.sonar.db.project.CreationMethod;
44 import org.sonar.db.user.UserTelemetryDto;
46 import static java.util.stream.Collectors.joining;
47 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
48 import static org.assertj.core.api.Assertions.assertThat;
49 import static org.mockito.Mockito.mock;
50 import static org.mockito.Mockito.when;
51 import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
52 import static org.sonar.db.newcodeperiod.NewCodePeriodType.PREVIOUS_VERSION;
53 import static org.sonar.test.JsonAssert.assertJson;
55 @RunWith(DataProviderRunner.class)
56 public class TelemetryDataJsonWriterTest {
58 private final Random random = new Random();
60 private final TelemetryExtension extension = mock(TelemetryExtension.class);
62 private final System2 system2 = mock(System2.class);
64 private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
66 private static final int NCD_ID = 12345;
68 private static final TelemetryData.NewCodeDefinition NCD_INSTANCE = new TelemetryData.NewCodeDefinition(PREVIOUS_VERSION.name(), "", "instance");
69 private static final TelemetryData.NewCodeDefinition NCD_PROJECT = new TelemetryData.NewCodeDefinition(NUMBER_OF_DAYS.name(), "30", "project");
72 public void write_server_id_version_and_sequence() {
73 TelemetryData data = telemetryBuilder().build();
75 String json = writeTelemetryData(data);
76 assertJson(json).isSimilarTo("""
80 "messageSequenceNumber": %s
82 """.formatted(data.getServerId(), data.getVersion(), data.getMessageSequenceNumber()));
86 public void does_not_write_edition_if_null() {
87 TelemetryData data = telemetryBuilder().build();
89 String json = writeTelemetryData(data);
91 assertThat(json).doesNotContain("edition");
95 @UseDataProvider("allEditions")
96 public void writes_edition_if_non_null(EditionProvider.Edition edition) {
97 TelemetryData data = telemetryBuilder()
101 String json = writeTelemetryData(data);
102 assertJson(json).isSimilarTo("""
106 """.formatted(edition.name().toLowerCase(Locale.ENGLISH)));
110 public void writes_default_qg() {
111 TelemetryData data = telemetryBuilder()
112 .setDefaultQualityGate("default-qg")
115 String json = writeTelemetryData(data);
116 assertJson(json).isSimilarTo("""
118 "defaultQualityGate": "%s"
120 """.formatted(data.getDefaultQualityGate()));
124 public void writes_database() {
125 String name = randomAlphabetic(12);
126 String version = randomAlphabetic(10);
127 TelemetryData data = telemetryBuilder()
128 .setDatabase(new TelemetryData.Database(name, version))
131 String json = writeTelemetryData(data);
132 assertJson(json).isSimilarTo("""
139 """.formatted(name, version));
143 public void writes_no_plugins() {
144 TelemetryData data = telemetryBuilder()
145 .setPlugins(Collections.emptyMap())
148 String json = writeTelemetryData(data);
150 assertJson(json).isSimilarTo("""
158 public void writes_all_plugins() {
159 Map<String, String> plugins = IntStream.range(0, 1 + random.nextInt(10))
161 .collect(Collectors.toMap(i -> "P" + i, i1 -> "V" + i1));
162 TelemetryData data = telemetryBuilder()
166 String json = writeTelemetryData(data);
167 assertJson(json).isSimilarTo("""
171 """.formatted(plugins.entrySet().stream().map(e -> "{\"name\":\"" + e.getKey() + "\",\"version\":\"" + e.getValue() + "\"}").collect(joining(","))));
175 public void does_not_write_installation_date_if_null() {
176 TelemetryData data = telemetryBuilder()
177 .setInstallationDate(null)
180 String json = writeTelemetryData(data);
182 assertThat(json).doesNotContain("installationDate");
186 public void write_installation_date_in_utc_format() {
187 TelemetryData data = telemetryBuilder()
188 .setInstallationDate(1_000L)
191 String json = writeTelemetryData(data);
193 assertJson(json).isSimilarTo("""
195 "installationDate":"1970-01-01T00:00:01+0000"
201 public void does_not_write_installation_version_if_null() {
202 TelemetryData data = telemetryBuilder()
203 .setInstallationVersion(null)
206 String json = writeTelemetryData(data);
208 assertThat(json).doesNotContain("installationVersion");
212 public void write_installation_version() {
213 String installationVersion = randomAlphabetic(5);
214 TelemetryData data = telemetryBuilder()
215 .setInstallationVersion(installationVersion)
218 String json = writeTelemetryData(data);
219 assertJson(json).isSimilarTo("""
221 "installationVersion": "%s"
223 """.formatted(installationVersion));
227 @UseDataProvider("getFeatureFlagEnabledStates")
228 public void write_container_flag(boolean isIncontainer) {
229 TelemetryData data = telemetryBuilder()
230 .setInContainer(isIncontainer)
233 String json = writeTelemetryData(data);
234 assertJson(json).isSimilarTo("""
238 """.formatted(isIncontainer));
242 public static Object[][] getManagedInstanceData() {
243 return new Object[][] {
252 @UseDataProvider("getManagedInstanceData")
253 public void writeTelemetryData_encodesCorrectlyManagedInstanceInformation(boolean isManaged, String provider) {
254 TelemetryData data = telemetryBuilder()
255 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(isManaged, provider))
258 String json = writeTelemetryData(data);
261 assertJson(json).isSimilarTo("""
263 "managedInstanceInformation": {
268 """.formatted(provider));
270 assertJson(json).isSimilarTo("""
272 "managedInstanceInformation": {
281 public void writeTelemetryData_shouldWriteCloudUsage() {
282 TelemetryData data = telemetryBuilder().build();
284 String json = writeTelemetryData(data);
285 assertJson(json).isSimilarTo("""
289 "kubernetesVersion": "1.27",
290 "kubernetesPlatform": "linux/amd64",
291 "kubernetesProvider": "5.4.181-99.354.amzn2.x86_64",
292 "officialHelmChart": "10.1.0",
293 "officialImage": false,
294 "containerRuntime": "docker"
301 public void writes_has_unanalyzed_languages() {
302 TelemetryData data = telemetryBuilder()
303 .setHasUnanalyzedC(true)
304 .setHasUnanalyzedCpp(false)
307 String json = writeTelemetryData(data);
309 assertJson(json).isSimilarTo("""
311 "hasUnanalyzedC": true,
312 "hasUnanalyzedCpp": false,
318 public void writes_security_custom_config() {
319 TelemetryData data = telemetryBuilder()
320 .setCustomSecurityConfigs(Set.of("php", "java"))
323 String json = writeTelemetryData(data);
325 assertJson(json).isSimilarTo("""
327 "customSecurityConfig": ["php", "java"]
333 public void writes_local_timestamp() {
334 when(system2.now()).thenReturn(1000L);
336 TelemetryData data = telemetryBuilder().build();
337 String json = writeTelemetryData(data);
339 assertJson(json).isSimilarTo("""
341 "localTimestamp": "1970-01-01T00:00:01+0000"
347 public void writes_all_users_with_anonymous_md5_uuids() {
348 TelemetryData data = telemetryBuilder()
349 .setUsers(attachUsers())
352 String json = writeTelemetryData(data);
354 assertJson(json).isSimilarTo("""
360 "identityProvider": "gitlab",
361 "lastActivity": "1970-01-01T00:00:00+0000",
362 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
367 "status": "inactive",
368 "identityProvider": "gitlab",
369 "lastActivity": "1970-01-01T00:00:00+0000",
370 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
376 "identityProvider": "gitlab",
377 "lastActivity": "1970-01-01T00:00:00+0000",
378 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
384 .formatted(DigestUtils.sha3_224Hex("uuid-0"), DigestUtils.sha3_224Hex("uuid-1"), DigestUtils.sha3_224Hex("uuid-2")));
388 public void writes_all_projects() {
389 TelemetryData data = telemetryBuilder()
390 .setProjects(attachProjects())
393 String json = writeTelemetryData(data);
395 assertJson(json).isSimilarTo("""
399 "projectUuid": "uuid-0",
400 "lastAnalysis": "1970-01-01T00:00:00+0000",
401 "language": "lang-0",
402 "qualityProfile" : "qprofile-0",
406 "projectUuid": "uuid-1",
407 "lastAnalysis": "1970-01-01T00:00:00+0000",
408 "language": "lang-1",
409 "qualityProfile" : "qprofile-1",
413 "projectUuid": "uuid-2",
414 "lastAnalysis": "1970-01-01T00:00:00+0000",
415 "language": "lang-2",
416 "qualityProfile" : "qprofile-2",
425 public void writeTelemetryData_whenAnalyzedLanguages_shouldwriteAllProjectsStats() {
426 TelemetryData data = telemetryBuilder()
427 .setProjectStatistics(attachProjectStatsWithMetrics())
430 String json = writeTelemetryData(data);
432 assertJson(json).isSimilarTo("""
434 "projects-general-stats": [
436 "projectUuid": "uuid-0",
438 "pullRequestCount": 2,
439 "qualityGate": "qg-0",
442 "devopsPlatform": "devops-0",
444 "vulnerabilities": 3,
445 "securityHotspots": 4,
447 "developmentCost": 30,
449 "externalSecurityReportExportedAt": 1500000,
450 "project_creation_method": "LOCAL_API"
453 "projectUuid": "uuid-1",
455 "pullRequestCount": 4,
456 "qualityGate": "qg-1",
459 "devopsPlatform": "devops-1",
461 "vulnerabilities": 6,
462 "securityHotspots": 8,
463 "technicalDebt": 120,
464 "developmentCost": 60,
466 "externalSecurityReportExportedAt": 1500001,
467 "project_creation_method": "LOCAL_API"
470 "projectUuid": "uuid-2",
472 "pullRequestCount": 6,
473 "qualityGate": "qg-2",
476 "devopsPlatform": "devops-2",
478 "vulnerabilities": 9,
479 "securityHotspots": 12,
480 "technicalDebt": 180,
481 "developmentCost": 90,
483 "externalSecurityReportExportedAt": 1500002,
484 "project_creation_method": "LOCAL_API"
492 public void writes_all_projects_stats_with_unanalyzed_languages() {
493 TelemetryData data = telemetryBuilder()
494 .setProjectStatistics(attachProjectStats())
497 String json = writeTelemetryData(data);
498 assertThat(json).doesNotContain("hasUnanalyzedC", "hasUnanalyzedCpp");
502 public void writes_all_projects_stats_without_missing_metrics() {
503 TelemetryData data = telemetryBuilder()
504 .setProjectStatistics(attachProjectStats())
506 String json = writeTelemetryData(data);
507 assertThat(json).doesNotContain("bugs", "vulnerabilities", "securityHotspots", "technicalDebt", "developmentCost");
511 public void writes_all_quality_gates() {
512 TelemetryData data = telemetryBuilder()
513 .setQualityGates(attachQualityGates())
516 String json = writeTelemetryData(data);
517 assertJson(json).isSimilarTo("""
522 "caycStatus": "non-compliant"
526 "caycStatus": "compliant"
530 "caycStatus": "over-compliant"
538 public void writeTelemetryData_shouldWriteQualityProfiles() {
539 TelemetryData data = telemetryBuilder()
540 .setQualityProfiles(List.of(
541 new TelemetryData.QualityProfile("uuid-1", "parent-uuid-1", "js", true, false, true, 2, 3, 4),
542 new TelemetryData.QualityProfile("uuid-1", null, "js", false, true, null, null, null, null)))
545 String json = writeTelemetryData(data);
546 assertJson(json).isSimilarTo("""
548 "quality-profiles": [
551 "parentUuid": "parent-uuid-1",
555 "builtInParent": true,
556 "rulesOverriddenCount": 2,
557 "rulesActivatedCount": 3,
558 "rulesDeactivatedCount": 4
571 public void writes_all_branches() {
572 TelemetryData data = telemetryBuilder()
573 .setBranches(attachBranches())
576 String json = writeTelemetryData(data);
577 assertJson(json).isSimilarTo("""
581 "projectUuid": "projectUuid1",
582 "branchUuid": "branchUuid1",
584 "greenQualityGateCount": 1,
586 "excludeFromPurge": true
589 "projectUuid": "projectUuid2",
590 "branchUuid": "branchUuid2",
592 "greenQualityGateCount": 0,
594 "excludeFromPurge": true
602 public void writes_new_code_definitions() {
603 TelemetryData data = telemetryBuilder()
604 .setNewCodeDefinitions(attachNewCodeDefinitions())
607 String json = writeTelemetryData(data);
608 assertJson(json).isSimilarTo("""
610 "new-code-definitions": [
626 """.formatted(NCD_INSTANCE.hashCode(), NCD_INSTANCE.type(), NCD_INSTANCE.value(), NCD_INSTANCE.scope(), NCD_PROJECT.hashCode(),
627 NCD_PROJECT.type(), NCD_PROJECT.value(), NCD_PROJECT.scope()));
631 public void writes_instance_new_code_definition() {
632 TelemetryData data = telemetryBuilder().build();
634 String json = writeTelemetryData(data);
635 assertThat(json).contains("ncdId");
639 private static TelemetryData.Builder telemetryBuilder() {
640 return TelemetryData.builder()
643 .setMessageSequenceNumber(1L)
644 .setPlugins(Collections.emptyMap())
645 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(false, null))
646 .setCloudUsage(new TelemetryData.CloudUsage(true, "1.27", "linux/amd64", "5.4.181-99.354.amzn2.x86_64", "10.1.0", "docker", false))
647 .setDatabase(new TelemetryData.Database("H2", "11"))
652 private static List<UserTelemetryDto> attachUsers() {
653 return IntStream.range(0, 3)
655 i -> new UserTelemetryDto().setUuid("uuid-" + i).setActive(i % 2 == 0).setLastConnectionDate(1L)
656 .setLastSonarlintConnectionDate(2L).setExternalIdentityProvider("gitlab").setScimUuid(i % 2 == 0 ? "scim-uuid-" + i : null))
660 private static List<TelemetryData.Project> attachProjects() {
661 return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.Project("uuid-" + i, 1L, "lang-" + i, "qprofile-" + i, (i + 1L) * 2)).toList();
664 private static List<TelemetryData.ProjectStatistics> attachProjectStatsWithMetrics() {
665 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsWithMetricBuilder(i).build()).toList();
668 private static List<TelemetryData.ProjectStatistics> attachProjectStats() {
669 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsBuilder(i).build()).toList();
672 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsBuilder(int i) {
673 return new TelemetryData.ProjectStatistics.Builder()
674 .setProjectUuid("uuid-" + i)
675 .setBranchCount((i + 1L) * 2L)
676 .setPRCount((i + 1L) * 2L)
677 .setQG("qg-" + i).setCi("ci-" + i)
679 .setDevops("devops-" + i)
681 .setCreationMethod(CreationMethod.LOCAL_API);
684 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsWithMetricBuilder(int i) {
685 return getProjectStatisticsBuilder(i)
686 .setBugs((i + 1L) * 2)
687 .setVulnerabilities((i + 1L) * 3)
688 .setSecurityHotspots((i + 1L) * 4)
689 .setDevelopmentCost((i + 1L) * 30d)
690 .setTechnicalDebt((i + 1L) * 60d)
691 .setExternalSecurityReportExportedAt(1_500_000L + i)
692 .setCreationMethod(CreationMethod.LOCAL_API);
695 private List<TelemetryData.QualityGate> attachQualityGates() {
696 return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"),
697 new TelemetryData.QualityGate("uuid-1", "compliant"),
698 new TelemetryData.QualityGate("uuid-2", "over-compliant"));
701 private List<TelemetryData.Branch> attachBranches() {
702 return List.of(new TelemetryData.Branch("projectUuid1", "branchUuid1", NCD_ID, 1, 2, true),
703 new TelemetryData.Branch("projectUuid2", "branchUuid2", NCD_ID, 0, 2, true));
706 private List<TelemetryData.NewCodeDefinition> attachNewCodeDefinitions() {
707 return List.of(NCD_INSTANCE, NCD_PROJECT);
711 public static Object[][] allEditions() {
712 return Arrays.stream(EditionProvider.Edition.values())
713 .map(t -> new Object[] {t})
714 .toArray(Object[][]::new);
717 private String writeTelemetryData(TelemetryData data) {
718 StringWriter jsonString = new StringWriter();
719 try (JsonWriter json = JsonWriter.of(jsonString)) {
720 underTest.writeTelemetryData(json, data);
722 return jsonString.toString();
726 public static Set<Boolean> getFeatureFlagEnabledStates() {
727 return Set.of(true, false);