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.core.util.stream.MoreCollectors;
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 =
69 new TelemetryData.NewCodeDefinition(PREVIOUS_VERSION.name(), "", "instance");
70 private static final TelemetryData.NewCodeDefinition NCD_PROJECT =
71 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_database() {
127 String name = randomAlphabetic(12);
128 String version = randomAlphabetic(10);
129 TelemetryData data = telemetryBuilder()
130 .setDatabase(new TelemetryData.Database(name, version))
133 String json = writeTelemetryData(data);
134 assertJson(json).isSimilarTo("""
141 """.formatted(name, version));
145 public void writes_no_plugins() {
146 TelemetryData data = telemetryBuilder()
147 .setPlugins(Collections.emptyMap())
150 String json = writeTelemetryData(data);
152 assertJson(json).isSimilarTo("""
160 public void writes_all_plugins() {
161 Map<String, String> plugins = IntStream.range(0, 1 + random.nextInt(10))
163 .collect(MoreCollectors.uniqueIndex(i -> "P" + i, i -> "V" + i));
164 TelemetryData data = telemetryBuilder()
168 String json = writeTelemetryData(data);
169 assertJson(json).isSimilarTo("""
173 """.formatted(plugins.entrySet().stream().map(e -> "{\"name\":\"" + e.getKey() + "\",\"version\":\"" + e.getValue() + "\"}").collect(joining(","))));
177 public void does_not_write_installation_date_if_null() {
178 TelemetryData data = telemetryBuilder()
179 .setInstallationDate(null)
182 String json = writeTelemetryData(data);
184 assertThat(json).doesNotContain("installationDate");
188 public void write_installation_date_in_utc_format() {
189 TelemetryData data = telemetryBuilder()
190 .setInstallationDate(1_000L)
193 String json = writeTelemetryData(data);
195 assertJson(json).isSimilarTo("""
197 "installationDate":"1970-01-01T00:00:01+0000"
203 public void does_not_write_installation_version_if_null() {
204 TelemetryData data = telemetryBuilder()
205 .setInstallationVersion(null)
208 String json = writeTelemetryData(data);
210 assertThat(json).doesNotContain("installationVersion");
214 public void write_installation_version() {
215 String installationVersion = randomAlphabetic(5);
216 TelemetryData data = telemetryBuilder()
217 .setInstallationVersion(installationVersion)
220 String json = writeTelemetryData(data);
221 assertJson(json).isSimilarTo("""
223 "installationVersion": "%s"
225 """.formatted(installationVersion));
229 @UseDataProvider("getFeatureFlagEnabledStates")
230 public void write_docker_flag(boolean isInDocker) {
231 TelemetryData data = telemetryBuilder()
232 .setInDocker(isInDocker)
235 String json = writeTelemetryData(data);
236 assertJson(json).isSimilarTo("""
240 """.formatted(isInDocker));
244 public static Object[][] getManagedInstanceData() {
245 return new Object[][] {
254 @UseDataProvider("getManagedInstanceData")
255 public void writeTelemetryData_encodesCorrectlyManagedInstanceInformation(boolean isManaged, String provider) {
256 TelemetryData data = telemetryBuilder()
257 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(isManaged, provider))
260 String json = writeTelemetryData(data);
263 assertJson(json).isSimilarTo("""
265 "managedInstanceInformation": {
270 """.formatted(provider));
272 assertJson(json).isSimilarTo("""
274 "managedInstanceInformation": {
283 public void writes_has_unanalyzed_languages() {
284 TelemetryData data = telemetryBuilder()
285 .setHasUnanalyzedC(true)
286 .setHasUnanalyzedCpp(false)
289 String json = writeTelemetryData(data);
291 assertJson(json).isSimilarTo("""
293 "hasUnanalyzedC": true,
294 "hasUnanalyzedCpp": false,
300 public void writes_security_custom_config() {
301 TelemetryData data = telemetryBuilder()
302 .setCustomSecurityConfigs(Set.of("php", "java"))
305 String json = writeTelemetryData(data);
307 assertJson(json).isSimilarTo("""
309 "customSecurityConfig": ["php", "java"]
315 public void writes_local_timestamp() {
316 when(system2.now()).thenReturn(1000L);
318 TelemetryData data = telemetryBuilder().build();
319 String json = writeTelemetryData(data);
321 assertJson(json).isSimilarTo("""
323 "localTimestamp": "1970-01-01T00:00:01+0000"
329 public void writes_all_users_with_anonymous_md5_uuids() {
330 TelemetryData data = telemetryBuilder()
331 .setUsers(attachUsers())
334 String json = writeTelemetryData(data);
336 assertJson(json).isSimilarTo("""
342 "identityProvider": "gitlab",
343 "lastActivity": "1970-01-01T00:00:00+0000",
344 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
349 "status": "inactive",
350 "identityProvider": "gitlab",
351 "lastActivity": "1970-01-01T00:00:00+0000",
352 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
358 "identityProvider": "gitlab",
359 "lastActivity": "1970-01-01T00:00:00+0000",
360 "lastSonarlintActivity": "1970-01-01T00:00:00+0000",
366 .formatted(DigestUtils.sha3_224Hex("uuid-0"), DigestUtils.sha3_224Hex("uuid-1"), DigestUtils.sha3_224Hex("uuid-2")));
370 public void writes_all_projects() {
371 TelemetryData data = telemetryBuilder()
372 .setProjects(attachProjects())
375 String json = writeTelemetryData(data);
377 assertJson(json).isSimilarTo("""
381 "projectUuid": "uuid-0",
382 "lastAnalysis": "1970-01-01T00:00:00+0000",
383 "language": "lang-0",
387 "projectUuid": "uuid-1",
388 "lastAnalysis": "1970-01-01T00:00:00+0000",
389 "language": "lang-1",
393 "projectUuid": "uuid-2",
394 "lastAnalysis": "1970-01-01T00:00:00+0000",
395 "language": "lang-2",
404 public void writeTelemetryData_whenAnalyzedLanguages_shouldwriteAllProjectsStats() {
405 TelemetryData data = telemetryBuilder()
406 .setProjectStatistics(attachProjectStatsWithMetrics())
409 String json = writeTelemetryData(data);
411 assertJson(json).isSimilarTo("""
413 "projects-general-stats": [
415 "projectUuid": "uuid-0",
417 "pullRequestCount": 2,
418 "qualityGate": "qg-0",
421 "devopsPlatform": "devops-0",
423 "vulnerabilities": 3,
424 "securityHotspots": 4,
426 "developmentCost": 30,
430 "projectUuid": "uuid-1",
432 "pullRequestCount": 4,
433 "qualityGate": "qg-1",
436 "devopsPlatform": "devops-1",
438 "vulnerabilities": 6,
439 "securityHotspots": 8,
440 "technicalDebt": 120,
441 "developmentCost": 60,
445 "projectUuid": "uuid-2",
447 "pullRequestCount": 6,
448 "qualityGate": "qg-2",
451 "devopsPlatform": "devops-2",
453 "vulnerabilities": 9,
454 "securityHotspots": 12,
455 "technicalDebt": 180,
456 "developmentCost": 90,
466 public void writes_all_projects_stats_with_unanalyzed_languages() {
467 TelemetryData data = telemetryBuilder()
468 .setProjectStatistics(attachProjectStats())
471 String json = writeTelemetryData(data);
472 assertThat(json).doesNotContain("hasUnanalyzedC", "hasUnanalyzedCpp");
476 public void writes_all_projects_stats_without_missing_metrics() {
477 TelemetryData data = telemetryBuilder()
478 .setProjectStatistics(attachProjectStats())
480 String json = writeTelemetryData(data);
481 assertThat(json).doesNotContain("bugs", "vulnerabilities", "securityHotspots", "technicalDebt", "developmentCost");
485 public void writes_all_quality_gates() {
486 TelemetryData data = telemetryBuilder()
487 .setQualityGates(attachQualityGates())
490 String json = writeTelemetryData(data);
491 assertJson(json).isSimilarTo("""
496 "caycStatus": "non-compliant"
500 "caycStatus": "compliant"
504 "caycStatus": "over-compliant"
513 public void writes_all_branches() {
514 TelemetryData data = telemetryBuilder()
515 .setBranches(attachBranches())
518 String json = writeTelemetryData(data);
519 assertJson(json).isSimilarTo("""
523 "projectUuid": "projectUuid1",
524 "branchUuid": "branchUuid1",
526 "greenQualityGateCount": 1,
528 "excludeFromPurge": true
531 "projectUuid": "projectUuid2",
532 "branchUuid": "branchUuid2",
534 "greenQualityGateCount": 0,
536 "excludeFromPurge": true
544 public void writes_new_code_definitions() {
545 TelemetryData data = telemetryBuilder()
546 .setNewCodeDefinitions(attachNewCodeDefinitions())
549 String json = writeTelemetryData(data);
550 assertJson(json).isSimilarTo("""
552 "new-code-definitions": [
568 """.formatted(NCD_INSTANCE.hashCode(), NCD_INSTANCE.type(), NCD_INSTANCE.value(), NCD_INSTANCE.scope(), NCD_PROJECT.hashCode(),
569 NCD_PROJECT.type(), NCD_PROJECT.value(), NCD_PROJECT.scope()));
573 public void writes_instance_new_code_definition() {
574 TelemetryData data = telemetryBuilder().build();
576 String json = writeTelemetryData(data);
577 assertThat(json).contains("ncdId");
581 private static TelemetryData.Builder telemetryBuilder() {
582 return TelemetryData.builder()
585 .setMessageSequenceNumber(1L)
586 .setPlugins(Collections.emptyMap())
587 .setManagedInstanceInformation(new TelemetryData.ManagedInstanceInformation(false, null))
588 .setCloudUsage(new TelemetryData.CloudUsage(false))
589 .setDatabase(new TelemetryData.Database("H2", "11"))
594 private static List<UserTelemetryDto> attachUsers() {
595 return IntStream.range(0, 3)
597 i -> new UserTelemetryDto().setUuid("uuid-" + i).setActive(i % 2 == 0).setLastConnectionDate(1L)
598 .setLastSonarlintConnectionDate(2L).setExternalIdentityProvider("gitlab").setScimUuid(i % 2 == 0 ? "scim-uuid-" + i : null))
599 .collect(Collectors.toList());
602 private static List<TelemetryData.Project> attachProjects() {
603 return IntStream.range(0, 3).mapToObj(i -> new TelemetryData.Project("uuid-" + i, 1L, "lang-" + i, (i + 1L) * 2L)).collect(Collectors.toList());
606 private static List<TelemetryData.ProjectStatistics> attachProjectStatsWithMetrics() {
607 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsWithMetricBuilder(i).build()).toList();
610 private static List<TelemetryData.ProjectStatistics> attachProjectStats() {
611 return IntStream.range(0, 3).mapToObj(i -> getProjectStatisticsBuilder(i).build()).toList();
614 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsBuilder(int i) {
615 return new TelemetryData.ProjectStatistics.Builder()
616 .setProjectUuid("uuid-" + i)
617 .setBranchCount((i + 1L) * 2L)
618 .setPRCount((i + 1L) * 2L)
619 .setQG("qg-" + i).setCi("ci-" + i)
621 .setDevops("devops-" + i)
625 private static TelemetryData.ProjectStatistics.Builder getProjectStatisticsWithMetricBuilder(int i) {
626 return getProjectStatisticsBuilder(i)
627 .setBugs((i + 1L) * 2)
628 .setVulnerabilities((i + 1L) * 3)
629 .setSecurityHotspots((i + 1L) * 4)
630 .setDevelopmentCost((i + 1L) * 30d)
631 .setTechnicalDebt((i + 1L) * 60d);
634 private List<TelemetryData.QualityGate> attachQualityGates() {
635 return List.of(new TelemetryData.QualityGate("uuid-0", "non-compliant"),
636 new TelemetryData.QualityGate("uuid-1", "compliant"),
637 new TelemetryData.QualityGate("uuid-2", "over-compliant"));
640 private List<TelemetryData.Branch> attachBranches() {
641 return List.of(new TelemetryData.Branch("projectUuid1", "branchUuid1", NCD_ID, 1, 2, true),
642 new TelemetryData.Branch("projectUuid2", "branchUuid2", NCD_ID, 0, 2, true));
645 private List<TelemetryData.NewCodeDefinition> attachNewCodeDefinitions() {
646 return List.of(NCD_INSTANCE, NCD_PROJECT);
650 public static Object[][] allEditions() {
651 return Arrays.stream(EditionProvider.Edition.values())
652 .map(t -> new Object[] {t})
653 .toArray(Object[][]::new);
656 private String writeTelemetryData(TelemetryData data) {
657 StringWriter jsonString = new StringWriter();
658 try (JsonWriter json = JsonWriter.of(jsonString)) {
659 underTest.writeTelemetryData(json, data);
661 return jsonString.toString();
665 public static Set<Boolean> getFeatureFlagEnabledStates() {
666 return Set.of(true, false);