@@ -19,17 +19,19 @@ | |||
*/ | |||
package org.sonar.server.telemetry; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.core.platform.EditionProvider; | |||
import org.sonar.core.platform.EditionProvider.Edition; | |||
import org.sonar.db.user.UserTelemetryDto; | |||
import static java.util.Collections.emptyList; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.Objects.requireNonNullElse; | |||
public class TelemetryData { | |||
private final String serverId; | |||
@@ -41,10 +43,11 @@ public class TelemetryData { | |||
private final Long installationDate; | |||
private final String installationVersion; | |||
private final boolean inDocker; | |||
private final List<String> customSecurityConfigs; | |||
private final boolean isScimEnabled; | |||
private final List<UserTelemetryDto> users; | |||
private final List<Project> projects; | |||
private final List<ProjectStatistics> projectStatistics; | |||
private final Set<String> customSecurityConfigs; | |||
private TelemetryData(Builder builder) { | |||
serverId = builder.serverId; | |||
@@ -56,10 +59,11 @@ public class TelemetryData { | |||
installationDate = builder.installationDate; | |||
installationVersion = builder.installationVersion; | |||
inDocker = builder.inDocker; | |||
customSecurityConfigs = builder.customSecurityConfigs == null ? emptyList() : builder.customSecurityConfigs; | |||
isScimEnabled = builder.isScimEnabled; | |||
users = builder.users; | |||
projects = builder.projects; | |||
projectStatistics = builder.projectStatistics; | |||
customSecurityConfigs = requireNonNullElse(builder.customSecurityConfigs, Set.of()); | |||
} | |||
public String getServerId() { | |||
@@ -98,7 +102,11 @@ public class TelemetryData { | |||
return inDocker; | |||
} | |||
public List<String> getCustomSecurityConfigs() { | |||
public boolean isScimEnabled() { | |||
return isScimEnabled; | |||
} | |||
public Set<String> getCustomSecurityConfigs() { | |||
return customSecurityConfigs; | |||
} | |||
@@ -128,7 +136,8 @@ public class TelemetryData { | |||
private Long installationDate; | |||
private String installationVersion; | |||
private boolean inDocker = false; | |||
private List<String> customSecurityConfigs; | |||
private boolean isScimEnabled; | |||
private Set<String> customSecurityConfigs; | |||
private List<UserTelemetryDto> users; | |||
private List<Project> projects; | |||
private List<ProjectStatistics> projectStatistics; | |||
@@ -182,7 +191,7 @@ public class TelemetryData { | |||
return this; | |||
} | |||
Builder setCustomSecurityConfigs(List<String> customSecurityConfigs) { | |||
Builder setCustomSecurityConfigs(Set<String> customSecurityConfigs) { | |||
this.customSecurityConfigs = customSecurityConfigs; | |||
return this; | |||
} | |||
@@ -197,12 +206,13 @@ public class TelemetryData { | |||
return this; | |||
} | |||
TelemetryData build() { | |||
requireNonNull(serverId); | |||
requireNonNull(version); | |||
requireNonNull(plugins); | |||
requireNonNull(database); | |||
public Builder setIsScimEnabled(boolean isEnabled) { | |||
this.isScimEnabled = isEnabled; | |||
return this; | |||
} | |||
TelemetryData build() { | |||
requireNonNullValues(serverId, version, plugins, database); | |||
return new TelemetryData(this); | |||
} | |||
@@ -210,6 +220,10 @@ public class TelemetryData { | |||
this.projectStatistics = projectStatistics; | |||
return this; | |||
} | |||
private static void requireNonNullValues(Object... values) { | |||
Arrays.stream(values).forEach(Objects::requireNonNull); | |||
} | |||
} | |||
static class Database { |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.telemetry; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import java.time.Instant; | |||
import java.time.ZoneOffset; | |||
import java.time.format.DateTimeFormatter; | |||
@@ -31,7 +32,9 @@ import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT; | |||
public class TelemetryDataJsonWriter { | |||
public static final String LANGUAGE_PROP = "language"; | |||
@VisibleForTesting | |||
static final String SCIM_PROPERTY = "scim"; | |||
private static final String LANGUAGE_PROPERTY = "language"; | |||
public void writeTelemetryData(JsonWriter json, TelemetryData statistics) { | |||
json.beginObject(); | |||
@@ -69,6 +72,8 @@ public class TelemetryDataJsonWriter { | |||
} | |||
json.prop("docker", statistics.isInDocker()); | |||
json.prop(SCIM_PROPERTY, statistics.isScimEnabled()); | |||
writeUserData(json, statistics); | |||
writeProjectData(json, statistics); | |||
writeProjectStatsData(json, statistics); | |||
@@ -109,7 +114,7 @@ public class TelemetryDataJsonWriter { | |||
if (project.getLastAnalysis() != null) { | |||
json.prop("lastAnalysis", toUtc(project.getLastAnalysis())); | |||
} | |||
json.prop(LANGUAGE_PROP, project.getLanguage()); | |||
json.prop(LANGUAGE_PROPERTY, project.getLanguage()); | |||
json.prop("loc", project.getLoc()); | |||
json.endObject(); | |||
}); |
@@ -29,6 +29,7 @@ import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import org.apache.commons.codec.digest.DigestUtils; | |||
@@ -40,9 +41,11 @@ import org.sonar.core.platform.EditionProvider; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.user.UserTelemetryDto; | |||
import static java.lang.String.format; | |||
import static java.util.stream.Collectors.joining; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.telemetry.TelemetryDataJsonWriter.SCIM_PROPERTY; | |||
import static org.sonar.test.JsonAssert.assertJson; | |||
@RunWith(DataProviderRunner.class) | |||
@@ -210,23 +213,35 @@ public class TelemetryDataJsonWriterTest { | |||
} | |||
@Test | |||
public void write_docker_flag() { | |||
boolean inDocker = random.nextBoolean(); | |||
@UseDataProvider("getFeatureFlagEnabledStates") | |||
public void write_docker_flag(boolean isInDocker) { | |||
TelemetryData data = telemetryBuilder() | |||
.setInDocker(inDocker) | |||
.setInDocker(isInDocker) | |||
.build(); | |||
String json = writeTelemetryData(data); | |||
assertJson(json).isSimilarTo("{" + | |||
" \"docker\":" + inDocker + | |||
" \"docker\":" + isInDocker + | |||
"}"); | |||
} | |||
@Test | |||
@UseDataProvider("getFeatureFlagEnabledStates") | |||
public void write_scim_feature_flag(boolean isScimEnabled) { | |||
TelemetryData data = telemetryBuilder() | |||
.setIsScimEnabled(isScimEnabled) | |||
.build(); | |||
String json = writeTelemetryData(data); | |||
assertJson(json).isSimilarTo("{" + format(" \"%s\":", SCIM_PROPERTY) + isScimEnabled + "}"); | |||
} | |||
@Test | |||
public void writes_security_custom_config() { | |||
TelemetryData data = telemetryBuilder() | |||
.setCustomSecurityConfigs(Arrays.asList("php", "java")) | |||
.setCustomSecurityConfigs(Set.of("php", "java")) | |||
.build(); | |||
String json = writeTelemetryData(data); | |||
@@ -396,4 +411,9 @@ public class TelemetryDataJsonWriterTest { | |||
} | |||
return jsonString.toString(); | |||
} | |||
@DataProvider | |||
public static Set<Boolean> getFeatureFlagEnabledStates() { | |||
return Set.of(true, false); | |||
} | |||
} |
@@ -19,13 +19,14 @@ | |||
*/ | |||
package org.sonar.server.telemetry; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import java.sql.DatabaseMetaData; | |||
import java.sql.SQLException; | |||
import java.util.ArrayList; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
@@ -61,7 +62,16 @@ import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE; | |||
@ServerSide | |||
public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
public static final String UNDETECTED = "undetected"; | |||
@VisibleForTesting | |||
static final String SCIM_PROPERTY_ENABLED = "sonar.scim.enabled"; | |||
private static final String UNDETECTED = "undetected"; | |||
private static final Map<String, String> LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP = Map.of( | |||
"sonar.security.config.javasecurity", "java", | |||
"sonar.security.config.phpsecurity", "php", | |||
"sonar.security.config.pythonsecurity", "python", | |||
"sonar.security.config.roslyn.sonaranalyzer.security.cs", "csharp"); | |||
private final Server server; | |||
private final DbClient dbClient; | |||
private final PluginRepository pluginRepository; | |||
@@ -72,6 +82,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
@CheckForNull | |||
private final LicenseReader licenseReader; | |||
@Inject | |||
public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository, | |||
PlatformEditionProvider editionProvider, InternalProperties internalProperties, Configuration configuration, | |||
@@ -121,9 +132,12 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
Optional<String> installationDateProperty = internalProperties.read(InternalProperties.INSTALLATION_DATE); | |||
installationDateProperty.ifPresent(s -> data.setInstallationDate(Long.valueOf(s))); | |||
Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION); | |||
data.setInstallationVersion(installationVersionProperty.orElse(null)); | |||
data.setInDocker(dockerSupport.isRunningInDocker()); | |||
return data.build(); | |||
return data | |||
.setInstallationVersion(installationVersionProperty.orElse(null)) | |||
.setInDocker(dockerSupport.isRunningInDocker()) | |||
.setIsScimEnabled(isScimEnabled()) | |||
.build(); | |||
} | |||
private void resolveProjectStatistics(TelemetryData.Builder data, DbSession dbSession) { | |||
@@ -185,18 +199,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
private void setSecurityCustomConfigIfPresent(TelemetryData.Builder data) { | |||
editionProvider.get() | |||
.filter(edition -> asList(ENTERPRISE, DATACENTER).contains(edition)) | |||
.ifPresent(edition -> { | |||
List<String> customSecurityConfigs = new LinkedList<>(); | |||
configuration.get("sonar.security.config.javasecurity") | |||
.ifPresent(s -> customSecurityConfigs.add("java")); | |||
configuration.get("sonar.security.config.phpsecurity") | |||
.ifPresent(s -> customSecurityConfigs.add("php")); | |||
configuration.get("sonar.security.config.pythonsecurity") | |||
.ifPresent(s -> customSecurityConfigs.add("python")); | |||
configuration.get("sonar.security.config.roslyn.sonaranalyzer.security.cs") | |||
.ifPresent(s -> customSecurityConfigs.add("csharp")); | |||
data.setCustomSecurityConfigs(customSecurityConfigs); | |||
}); | |||
.ifPresent(edition -> data.setCustomSecurityConfigs(getCustomerSecurityConfigurations())); | |||
} | |||
private Map<String, String> getAnalysisPropertyByProject(DbSession dbSession, String analysisPropertyKey) { | |||
@@ -214,13 +217,20 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
private static String getAlmName(String alm, String url) { | |||
if (checkIfCloudAlm(alm, ALM.GITHUB.getId(), url, "https://api.github.com")) { | |||
return "github_cloud"; | |||
} else if (checkIfCloudAlm(alm, ALM.GITLAB.getId(), url, "https://gitlab.com/api/v4")) { | |||
} | |||
if (checkIfCloudAlm(alm, ALM.GITLAB.getId(), url, "https://gitlab.com/api/v4")) { | |||
return "gitlab_cloud"; | |||
} else if (checkIfCloudAlm(alm, ALM.AZURE_DEVOPS.getId(), url, "https://dev.azure.com")) { | |||
} | |||
if (checkIfCloudAlm(alm, ALM.AZURE_DEVOPS.getId(), url, "https://dev.azure.com")) { | |||
return "azure_devops_cloud"; | |||
} else if (ALM.BITBUCKET_CLOUD.getId().equals(alm)) { | |||
} | |||
if (ALM.BITBUCKET_CLOUD.getId().equals(alm)) { | |||
return alm; | |||
} | |||
return alm + "_server"; | |||
} | |||
@@ -232,4 +242,19 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
public String loadServerId() { | |||
return server.getId(); | |||
} | |||
private Set<String> getCustomerSecurityConfigurations() { | |||
return LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP.keySet().stream() | |||
.filter(this::isPropertyPresentInConfiguration) | |||
.map(LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP::get) | |||
.collect(Collectors.toSet()); | |||
} | |||
private boolean isPropertyPresentInConfiguration(String property) { | |||
return configuration.get(property).isPresent(); | |||
} | |||
private boolean isScimEnabled() { | |||
return this.configuration.getBoolean(SCIM_PROPERTY_ENABLED).orElse(false); | |||
} | |||
} |
@@ -19,14 +19,21 @@ | |||
*/ | |||
package org.sonar.server.telemetry; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.sql.DatabaseMetaData; | |||
import java.sql.SQLException; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.impl.utils.TestSystem2; | |||
import org.sonar.core.platform.PlatformEditionProvider; | |||
@@ -39,6 +46,7 @@ import org.sonar.db.component.AnalysisPropertyDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.user.UserDbTester; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserTelemetryDto; | |||
import org.sonar.server.platform.DockerSupport; | |||
@@ -67,7 +75,9 @@ import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE; | |||
import static org.sonar.db.component.BranchType.BRANCH; | |||
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_KEY; | |||
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY; | |||
import static org.sonar.server.telemetry.TelemetryDataLoaderImpl.SCIM_PROPERTY_ENABLED; | |||
@RunWith(DataProviderRunner.class) | |||
public class TelemetryDataLoaderImplTest { | |||
private final static Long NOW = 100_000_000L; | |||
private final TestSystem2 system2 = new TestSystem2().setNow(NOW); | |||
@@ -101,10 +111,7 @@ public class TelemetryDataLoaderImplTest { | |||
when(pluginRepository.getPluginInfos()).thenReturn(plugins); | |||
when(editionProvider.get()).thenReturn(Optional.of(DEVELOPER)); | |||
int activeUserCount = 3; | |||
List<UserDto> activeUsers = IntStream.range(0, activeUserCount).mapToObj(i -> db.users().insertUser( | |||
u -> u.setExternalIdentityProvider("provider" + i).setLastSonarlintConnectionDate(i * 2L))) | |||
.collect(Collectors.toList()); | |||
List<UserDto> activeUsers = composeActiveUsers(3); | |||
// update last connection | |||
activeUsers.forEach(u -> db.users().updateLastConnectionDate(u, 5L)); | |||
@@ -178,6 +185,17 @@ public class TelemetryDataLoaderImplTest { | |||
tuple(1L, 0L, "scm-2", "ci-2", "github_cloud")); | |||
} | |||
private List<UserDto> composeActiveUsers(int count) { | |||
UserDbTester userDbTester = db.users(); | |||
Function<Integer, Consumer<UserDto>> userConfigurator = index -> user -> user.setExternalIdentityProvider("provider" + index).setLastSonarlintConnectionDate(index * 2L); | |||
return IntStream | |||
.rangeClosed(1, count) | |||
.mapToObj(userConfigurator::apply) | |||
.map(userDbTester::insertUser) | |||
.collect(Collectors.toList()); | |||
} | |||
private void assertDatabaseMetadata(TelemetryData.Database database) { | |||
try (DbSession dbSession = db.getDbClient().openSession(false)) { | |||
DatabaseMetaData metadata = dbSession.getConnection().getMetaData(); | |||
@@ -378,6 +396,17 @@ public class TelemetryDataLoaderImplTest { | |||
.containsExactlyInAnyOrder(tuple("undetected", "undetected", "undetected")); | |||
} | |||
@Test | |||
@UseDataProvider("getScimFeatureStatues") | |||
public void detect_scim_feature_status(boolean isEnabled) { | |||
db.components().insertPublicProject(); | |||
when(configuration.getBoolean(SCIM_PROPERTY_ENABLED)).thenReturn(Optional.of(isEnabled)); | |||
TelemetryData data = communityUnderTest.load(); | |||
assertThat(data.isScimEnabled()).isEqualTo(isEnabled); | |||
} | |||
private PluginInfo newPlugin(String key, String version) { | |||
return new PluginInfo(key) | |||
.setVersion(Version.create(version)); | |||
@@ -392,4 +421,8 @@ public class TelemetryDataLoaderImplTest { | |||
.setCreatedAt(1L)); | |||
} | |||
@DataProvider | |||
public static Set<Boolean> getScimFeatureStatues() { | |||
return Set.of(true, false); | |||
} | |||
} |