import okhttp3.RequestBody;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
@ServerSide
public class TelemetryClient {
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+ private static final Logger LOG = Loggers.get(TelemetryClient.class);
private final OkHttpClient okHttpClient;
private final TelemetryUrl serverUrl;
}
}
+ void optOut(String json) {
+ Request.Builder request = new Request.Builder();
+ request.url(serverUrl.get());
+ RequestBody body = RequestBody.create(JSON, json);
+ request.delete(body);
+
+ try {
+ okHttpClient.newCall(request.build()).execute();
+ } catch (IOException e) {
+ LOG.debug("Error when sending opt-out usage statistics: %s", e.getMessage());
+ }
+ }
+
private Request buildHttpRequest(String json) {
Request.Builder request = new Request.Builder();
request.url(serverUrl.get());
request.post(body);
return request.build();
}
+
}
import org.sonar.api.platform.Server;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.server.property.InternalProperties;
import static org.sonar.api.utils.DateUtils.formatDate;
import static org.sonar.api.utils.DateUtils.parseDate;
+import static org.sonar.server.telemetry.TelemetryProperties.PROP_ENABLE;
+import static org.sonar.server.telemetry.TelemetryProperties.PROP_URL;
@ServerSide
public class TelemetryDaemon implements Startable {
private static final String THREAD_NAME_PREFIX = "sq-telemetry-service-";
private static final int SEVEN_DAYS = 7 * 24 * 60 * 60 * 1_000;
private static final String I_PROP_LAST_PING = "sonar.telemetry.lastPing";
+ private static final String I_PROP_OPT_OUT = "sonar.telemetry.optOut";
+ private static final Logger LOG = Loggers.get(TelemetryDaemon.class);
private final TelemetryClient telemetryClient;
+ private final Configuration config;
private final InternalProperties internalProperties;
private final Server server;
private final System2 system2;
private ScheduledExecutorService executorService;
- public TelemetryDaemon(TelemetryClient telemetryClient, InternalProperties internalProperties, Server server, System2 system2, Configuration config) {
+ public TelemetryDaemon(TelemetryClient telemetryClient, Configuration config, InternalProperties internalProperties, Server server, System2 system2) {
this.telemetryClient = telemetryClient;
+ this.config = config;
+ this.frequencyInSeconds = new TelemetryFrequency(config);
this.internalProperties = internalProperties;
this.server = server;
- this.frequencyInSeconds = new TelemetryFrequency(config);
this.system2 = system2;
}
@Override
public void start() {
+ boolean isTelemetryActivated = config.getBoolean(PROP_ENABLE).orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", PROP_URL)));
+ if (!internalProperties.read(I_PROP_OPT_OUT).isPresent()) {
+ if (!isTelemetryActivated) {
+ StringWriter json = new StringWriter();
+ try (JsonWriter writer = JsonWriter.of(json)) {
+ writer.beginObject();
+ writer.prop("id", server.getId());
+ writer.endObject();
+ }
+ telemetryClient.optOut(json.toString());
+ internalProperties.write(I_PROP_OPT_OUT, String.valueOf(system2.now()));
+ LOG.info("Sharing of SonarQube statistics is disabled.");
+ } else {
+ internalProperties.write(I_PROP_OPT_OUT, null);
+ }
+ }
+
+ if (!isTelemetryActivated) {
+ return;
+ }
+ LOG.info("Sharing of SonarQube statistics is enabled.");
executorService = Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setNameFormat(THREAD_NAME_PREFIX + "%d")
@Properties({
@Property(
+ key = TelemetryProperties.PROP_ENABLE,
+ defaultValue = "true",
+ name = "Share SonarQube statistics",
+ global = false),
+ @Property(
key = TelemetryProperties.PROP_FREQUENCY,
// 6 hours in seconds
defaultValue = "21600",
global = false)
})
public class TelemetryProperties {
+ static final String PROP_ENABLE = "sonar.telemetry.enable";
static final String PROP_FREQUENCY = "sonar.telemetry.frequency";
static final String PROP_URL = "sonar.telemetry.url";
}
settings = new MapSettings(new PropertyDefinitions(TelemetryProperties.class));
system2.setNow(System.currentTimeMillis());
- underTest = new TelemetryDaemon(client, internalProperties, server, system2, settings.asConfig());
+ underTest = new TelemetryDaemon(client, settings.asConfig(), internalProperties, server, system2);
}
@Test
settings.setProperty("sonar.telemetry.frequency", "1");
underTest.start();
- verify(client, timeout(2_000).atLeastOnce()).send(anyString());
+ verify(client, timeout(1_000).atLeastOnce()).send(anyString());
}
@Test
long sevenDaysAgo = now - (ONE_DAY * 7L);
internalProperties.write("sonar.telemetry.lastPing", String.valueOf(sixDaysAgo));
settings.setProperty("sonar.telemetry.frequency", "1");
- underTest = new TelemetryDaemon(client, internalProperties, server, system2, settings.asConfig());
underTest.start();
verify(client, timeout(1_000).never()).send(anyString());
internalProperties.write("sonar.telemetry.lastPing", String.valueOf(sevenDaysAgo));
underTest.start();
- verify(client, timeout(2_000)).send(anyString());
+ verify(client, timeout(1_000).atLeastOnce()).send(anyString());
assertThat(internalProperties.read("sonar.telemetry.lastPing").get()).isEqualTo(String.valueOf(today));
}
+
+ @Test
+ public void opt_out_sent_once() {
+ settings.setProperty("sonar.telemetry.frequency", "1");
+ settings.setProperty("sonar.telemetry.enable", "false");
+ underTest.start();
+ underTest.start();
+
+
+ verify(client, timeout(1_000).never()).send(anyString());
+ verify(client, timeout(1_000).times(1)).optOut(anyString());
+ }
}
#sonar.path.data=data
#sonar.path.temp=temp
+# Telemetry - Share anonymous SonarQube statistics
+# By sharing anonymous SonarQube statistics, you help us understand how SonarQube is used so we can improve the product to work even better for you.
+# We don't collect source code or IP addresses. And we don't share the data with anyone else.
+# To see an example of the data shared: login as a global administrator, call the WS api/system/info and check the Statistics field.
+#sonar.telemetry.enable=true
+
#--------------------------------------------------------------------------------------------------
# DEVELOPMENT - only for developers
orchestrator.stop();
}
+
+ @Test
+ public void opt_out_of_telemetry() throws Exception {
+ String serverId = randomAlphanumeric(40);
+ orchestrator = Orchestrator.builderEnv()
+ .addPlugin(xooPlugin())
+ .setServerProperty("sonar.telemetry.enable", "false")
+ .setServerProperty("sonar.telemetry.url", url)
+ .setServerProperty("sonar.telemetry.frequency", "1")
+ .setServerProperty("sonar.core.id", serverId)
+ .build();
+ orchestrator.start();
+
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+
+ assertThat(request.getMethod()).isEqualTo("DELETE");
+ assertThat(request.getBody().readUtf8()).contains(serverId);
+ assertThat(request.getHeader(HttpHeaders.USER_AGENT)).contains("SonarQube");
+
+ orchestrator.stop();
+ }
}