]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17735 Telemetry improvements
authoralain <108417558+alain-kermis-sonarsource@users.noreply.github.com>
Mon, 19 Dec 2022 15:48:47 +0000 (16:48 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 19 Dec 2022 20:02:46 +0000 (20:02 +0000)
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java
server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java
server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java
sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java [new file with mode: 0644]

index c2825545c8aa85f1e99ea76a9032351dd47cd0fb..0870ff9b84580155feb6feae55a7e87fe2dc6354 100644 (file)
@@ -36,10 +36,10 @@ import static java.util.Objects.requireNonNullElse;
 public class TelemetryData {
   private final String serverId;
   private final String version;
+  private final Long messageSequenceNumber;
   private final Map<String, String> plugins;
   private final Database database;
   private final EditionProvider.Edition edition;
-  private final String licenseType;
   private final Long installationDate;
   private final String installationVersion;
   private final boolean inDocker;
@@ -52,10 +52,10 @@ public class TelemetryData {
   private TelemetryData(Builder builder) {
     serverId = builder.serverId;
     version = builder.version;
+    messageSequenceNumber = builder.messageSequenceNumber;
     plugins = builder.plugins;
     database = builder.database;
     edition = builder.edition;
-    licenseType = builder.licenseType;
     installationDate = builder.installationDate;
     installationVersion = builder.installationVersion;
     inDocker = builder.inDocker;
@@ -74,6 +74,10 @@ public class TelemetryData {
     return version;
   }
 
+  public Long getMessageSequenceNumber() {
+    return messageSequenceNumber;
+  }
+
   public Map<String, String> getPlugins() {
     return plugins;
   }
@@ -86,10 +90,6 @@ public class TelemetryData {
     return Optional.ofNullable(edition);
   }
 
-  public Optional<String> getLicenseType() {
-    return Optional.ofNullable(licenseType);
-  }
-
   public Long getInstallationDate() {
     return installationDate;
   }
@@ -129,10 +129,10 @@ public class TelemetryData {
   static class Builder {
     private String serverId;
     private String version;
+    private Long messageSequenceNumber;
     private Map<String, String> plugins;
     private Database database;
     private Edition edition;
-    private String licenseType;
     private Long installationDate;
     private String installationVersion;
     private boolean inDocker = false;
@@ -156,6 +156,11 @@ public class TelemetryData {
       return this;
     }
 
+    Builder setMessageSequenceNumber(@Nullable Long messageSequenceNumber) {
+      this.messageSequenceNumber = messageSequenceNumber;
+      return this;
+    }
+
     Builder setPlugins(Map<String, String> plugins) {
       this.plugins = plugins;
       return this;
@@ -171,11 +176,6 @@ public class TelemetryData {
       return this;
     }
 
-    Builder setLicenseType(@Nullable String licenseType) {
-      this.licenseType = licenseType;
-      return this;
-    }
-
     Builder setInstallationDate(@Nullable Long installationDate) {
       this.installationDate = installationDate;
       return this;
@@ -212,7 +212,7 @@ public class TelemetryData {
     }
 
     TelemetryData build() {
-      requireNonNullValues(serverId, version, plugins, database);
+      requireNonNullValues(serverId, version, plugins, database, messageSequenceNumber);
       return new TelemetryData(this);
     }
 
@@ -224,6 +224,7 @@ public class TelemetryData {
     private static void requireNonNullValues(Object... values) {
       Arrays.stream(values).forEach(Objects::requireNonNull);
     }
+
   }
 
   static class Database {
index de904d212e76c7b3605cc9816bdaa2125af64dec..e22b4e934b58bf1e11d7e07738046130eff20d06 100644 (file)
@@ -23,15 +23,27 @@ import com.google.common.annotations.VisibleForTesting;
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.util.List;
 import java.util.Locale;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.jetbrains.annotations.NotNull;
+import org.sonar.api.utils.System2;
 import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.telemetry.TelemetryExtension;
 
 import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
 
 public class TelemetryDataJsonWriter {
 
+  private final List<TelemetryExtension> extensions;
+
+  private final System2 system2;
+
+  public TelemetryDataJsonWriter(List<TelemetryExtension> extensions, System2 system2) {
+    this.extensions = extensions;
+    this.system2 = system2;
+  }
+
   @VisibleForTesting
   static final String SCIM_PROPERTY = "scim";
   private static final String LANGUAGE_PROPERTY = "language";
@@ -40,8 +52,9 @@ public class TelemetryDataJsonWriter {
     json.beginObject();
     json.prop("id", statistics.getServerId());
     json.prop("version", statistics.getVersion());
+    json.prop("messageSequenceNumber", statistics.getMessageSequenceNumber());
+    json.prop("localTimestamp", toUtc(system2.now()));
     statistics.getEdition().ifPresent(e -> json.prop("edition", e.name().toLowerCase(Locale.ENGLISH)));
-    statistics.getLicenseType().ifPresent(e -> json.prop("licenseType", e));
     json.name("database");
     json.beginObject();
     json.prop("name", statistics.getDatabase().getName());
@@ -78,6 +91,8 @@ public class TelemetryDataJsonWriter {
     writeProjectData(json, statistics);
     writeProjectStatsData(json, statistics);
 
+    extensions.forEach(e -> e.write(json));
+
     json.endObject();
   }
 
index b32a7aeb3b08355723d11eda6b5d0199ba1470a4..fad922797089394ca1078ac67b536d65485a9020 100644 (file)
@@ -36,8 +36,10 @@ import org.apache.commons.codec.digest.DigestUtils;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.sonar.api.utils.System2;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.telemetry.TelemetryExtension;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.user.UserTelemetryDto;
 
@@ -45,6 +47,8 @@ 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.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.server.telemetry.TelemetryDataJsonWriter.SCIM_PROPERTY;
 import static org.sonar.test.JsonAssert.assertJson;
 
@@ -53,17 +57,22 @@ public class TelemetryDataJsonWriterTest {
 
   private final Random random = new Random();
 
-  private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter();
+  private final TelemetryExtension extension = mock(TelemetryExtension.class);
+
+  private final System2 system2 = mock(System2.class);
+
+  private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
 
   @Test
-  public void write_server_id_and_version() {
+  public void write_server_id_version_and_sequence() {
     TelemetryData data = telemetryBuilder().build();
 
     String json = writeTelemetryData(data);
 
     assertJson(json).isSimilarTo("{" +
       "  \"id\": \"" + data.getServerId() + "\"," +
-      "  \"version\": \"" + data.getVersion() + "\"" +
+      "  \"version\": \"" + data.getVersion() + "\"," +
+      "  \"messageSequenceNumber\": " + data.getMessageSequenceNumber() +
       "}");
   }
 
@@ -90,29 +99,6 @@ public class TelemetryDataJsonWriterTest {
       "}");
   }
 
-  @Test
-  public void does_not_write_license_type_if_null() {
-    TelemetryData data = telemetryBuilder().build();
-
-    String json = writeTelemetryData(data);
-
-    assertThat(json).doesNotContain("licenseType");
-  }
-
-  @Test
-  public void writes_licenseType_if_non_null() {
-    String expected = randomAlphabetic(12);
-    TelemetryData data = telemetryBuilder()
-      .setLicenseType(expected)
-      .build();
-
-    String json = writeTelemetryData(data);
-
-    assertJson(json).isSimilarTo("{" +
-      "  \"licenseType\": \"" + expected + "\"" +
-      "}");
-  }
-
   @Test
   public void writes_database() {
     String name = randomAlphabetic(12);
@@ -251,6 +237,18 @@ public class TelemetryDataJsonWriterTest {
       "}");
   }
 
+  @Test
+  public void writes_local_timestamp() {
+    when(system2.now()).thenReturn(1000L);
+
+    TelemetryData data = telemetryBuilder().build();
+    String json = writeTelemetryData(data);
+
+    assertJson(json).isSimilarTo("{" +
+      "  \"localTimestamp\": \"1970-01-01T00:00:01+0000\"" +
+      "}");
+  }
+
   @Test
   public void writes_all_users_with_anonymous_md5_uuids() {
     TelemetryData data = telemetryBuilder()
@@ -376,6 +374,7 @@ public class TelemetryDataJsonWriterTest {
     return TelemetryData.builder()
       .setServerId("foo")
       .setVersion("bar")
+      .setMessageSequenceNumber(1L)
       .setPlugins(Collections.emptyMap())
       .setDatabase(new TelemetryData.Database("H2", "11"));
   }
index 80110a1a749711b14df29e47777f4876325919f4..0efaac7f130253ffbf17fa704c24523b8721bf07 100644 (file)
@@ -28,5 +28,6 @@ public interface LicenseReader {
 
   interface License {
     String getType();
+    Boolean isValidLicense();
   }
 }
index 1a5c0296c7552e1f381d5e3c4eea3feeeb40fc3f..106e97d0fd7adbdc113f4c821393f7b0ac3c7abe 100644 (file)
@@ -50,6 +50,7 @@ public class TelemetryDaemon implements Startable {
   private static final String LOCK_NAME = "TelemetryStat";
   private static final Logger LOG = Loggers.get(TelemetryDaemon.class);
   private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay";
+  static final String I_PROP_MESSAGE_SEQUENCE = "telemetry.messageSeq";
 
   private final TelemetryDataLoader dataLoader;
   private final TelemetryDataJsonWriter dataJsonWriter;
@@ -125,7 +126,7 @@ public class TelemetryDaemon implements Startable {
         long now = system2.now();
         if (shouldUploadStatistics(now)) {
           uploadStatistics();
-          internalProperties.write(I_PROP_LAST_PING, String.valueOf(now));
+          updateTelemetryProps(now);
         }
       } catch (Exception e) {
         LOG.debug("Error while checking SonarQube statistics: {}", e);
@@ -134,6 +135,19 @@ public class TelemetryDaemon implements Startable {
     };
   }
 
+  private void updateTelemetryProps(long now) {
+    internalProperties.write(I_PROP_LAST_PING, String.valueOf(now));
+
+    Optional<String> currentSequence = internalProperties.read(I_PROP_MESSAGE_SEQUENCE);
+    if (currentSequence.isEmpty()) {
+      internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(1));
+      return;
+    }
+
+    long current = Long.parseLong(currentSequence.get());
+    internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(current + 1));
+  }
+
   private void optOut() {
     StringWriter json = new StringWriter();
     try (JsonWriter writer = JsonWriter.of(json)) {
@@ -155,7 +169,7 @@ public class TelemetryDaemon implements Startable {
 
   private boolean shouldUploadStatistics(long now) {
     Optional<Long> lastPing = internalProperties.read(I_PROP_LAST_PING).map(Long::valueOf);
-    return !lastPing.isPresent() || now - lastPing.get() >= ONE_DAY;
+    return lastPing.isEmpty() || now - lastPing.get() >= ONE_DAY;
   }
 
   private int frequency() {
index 21f524078b19f5cc3f8f163e3898003a97ef1bc9..147d0a7c39a3ca0c03bfb982a15e923bc30d4612 100644 (file)
@@ -29,8 +29,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
 import javax.inject.Inject;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.platform.Server;
@@ -59,6 +57,7 @@ import static org.sonar.core.config.CorePropertyDefinitions.SONAR_ANALYSIS_DETEC
 import static org.sonar.core.platform.EditionProvider.Edition.COMMUNITY;
 import static org.sonar.core.platform.EditionProvider.Edition.DATACENTER;
 import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE;
+import static org.sonar.server.telemetry.TelemetryDaemon.I_PROP_MESSAGE_SEQUENCE;
 
 @ServerSide
 public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
@@ -79,14 +78,11 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
   private final Configuration configuration;
   private final InternalProperties internalProperties;
   private final DockerSupport dockerSupport;
-  @CheckForNull
-  private final LicenseReader licenseReader;
-
 
   @Inject
   public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository,
     PlatformEditionProvider editionProvider, InternalProperties internalProperties, Configuration configuration,
-    DockerSupport dockerSupport, @Nullable LicenseReader licenseReader) {
+    DockerSupport dockerSupport) {
     this.server = server;
     this.dbClient = dbClient;
     this.pluginRepository = pluginRepository;
@@ -94,7 +90,6 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
     this.internalProperties = internalProperties;
     this.configuration = configuration;
     this.dockerSupport = dockerSupport;
-    this.licenseReader = licenseReader;
   }
 
   private static Database loadDatabaseMetadata(DbSession dbSession) {
@@ -110,12 +105,10 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
   public TelemetryData load() {
     TelemetryData.Builder data = TelemetryData.builder();
 
+    data.setMessageSequenceNumber(retrieveCurrentMessageSequenceNumber() + 1);
     data.setServerId(server.getId());
     data.setVersion(server.getVersion());
     data.setEdition(editionProvider.get().orElse(null));
-    ofNullable(licenseReader)
-      .flatMap(reader -> licenseReader.read())
-      .ifPresent(license -> data.setLicenseType(license.getType()));
     Function<PluginInfo, String> getVersion = plugin -> plugin.getVersion() == null ? "undefined" : plugin.getVersion().getName();
     Map<String, String> plugins = pluginRepository.getPluginInfos().stream().collect(MoreCollectors.uniqueIndex(PluginInfo::getKey, getVersion));
     data.setPlugins(plugins);
@@ -140,6 +133,10 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
       .build();
   }
 
+  private Long retrieveCurrentMessageSequenceNumber() {
+    return internalProperties.read(I_PROP_MESSAGE_SEQUENCE).map(Long::parseLong).orElse(0L);
+  }
+
   private void resolveProjectStatistics(TelemetryData.Builder data, DbSession dbSession) {
     List<String> projectUuids = dbClient.projectDao().selectAllProjectUuids(dbSession);
     Map<String, String> scmByProject = getAnalysisPropertyByProject(dbSession, SONAR_ANALYSIS_DETECTEDSCM);
index ba4186af014fdc8b010616eea6daa0c941eac9bd..330bcaa0614180a28e14432e41b9efdee8c48b31 100644 (file)
@@ -62,6 +62,7 @@ public class TelemetryDaemonTest {
   private static final TelemetryData SOME_TELEMETRY_DATA = TelemetryData.builder()
     .setServerId("foo")
     .setVersion("bar")
+    .setMessageSequenceNumber(1L)
     .setPlugins(Collections.emptyMap())
     .setDatabase(new TelemetryData.Database("H2", "11"))
     .build();
@@ -153,6 +154,7 @@ public class TelemetryDaemonTest {
     long oneDayAgo = today - ONE_DAY - ONE_HOUR;
     internalProperties.write("telemetry.lastPing", String.valueOf(oneDayAgo));
     reset(internalProperties);
+    when(dataLoader.load()).thenReturn(SOME_TELEMETRY_DATA);
     mockDataJsonWriterDoingSomething();
 
     underTest.start();
@@ -177,6 +179,36 @@ public class TelemetryDaemonTest {
     assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is disabled.");
   }
 
+  @Test
+  public void write_sequence_as_one_if_not_previously_present() {
+    initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+    settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+    mockDataJsonWriterDoingSomething();
+
+    underTest.start();
+
+    verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "1");
+  }
+
+  @Test
+  public void write_sequence_correctly_incremented() {
+    initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+    settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+    internalProperties.write("telemetry.messageSeq", "10");
+    mockDataJsonWriterDoingSomething();
+
+    underTest.start();
+
+    verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "10");
+
+    // force another ping
+    internalProperties.write("telemetry.lastPing", String.valueOf(system2.now() - ONE_DAY));
+
+    verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "11");
+  }
+
   private void initTelemetrySettingsToDefaultValues() {
     settings.setProperty(SONAR_TELEMETRY_ENABLE.getKey(), SONAR_TELEMETRY_ENABLE.getDefaultValue());
     settings.setProperty(SONAR_TELEMETRY_URL.getKey(), SONAR_TELEMETRY_URL.getDefaultValue());
index 1e384cdeba629fb9c6054aee84b73219ef0b27c8..34ceb0b210cea57a4af5edbe324913cf4259d2db 100644 (file)
@@ -55,7 +55,6 @@ import org.sonar.server.property.MapInternalProperties;
 import org.sonar.updatecenter.common.Version;
 
 import static java.util.Arrays.asList;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.entry;
@@ -91,12 +90,11 @@ public class TelemetryDataLoaderImplTest {
   private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
   private final DockerSupport dockerSupport = mock(DockerSupport.class);
   private final InternalProperties internalProperties = spy(new MapInternalProperties());
-  private final LicenseReader licenseReader = mock(LicenseReader.class);
 
   private final TelemetryDataLoader communityUnderTest = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, editionProvider,
-      internalProperties, configuration, dockerSupport, null);
+      internalProperties, configuration, dockerSupport);
   private final TelemetryDataLoader commercialUnderTest = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, editionProvider,
-      internalProperties, configuration, dockerSupport, licenseReader);
+      internalProperties, configuration, dockerSupport);
 
   @Test
   public void send_telemetry_data() {
@@ -157,6 +155,7 @@ public class TelemetryDataLoaderImplTest {
     assertThat(data.getServerId()).isEqualTo(serverId);
     assertThat(data.getVersion()).isEqualTo(version);
     assertThat(data.getEdition()).contains(DEVELOPER);
+    assertThat(data.getMessageSequenceNumber()).isOne();
     assertDatabaseMetadata(data.getDatabase());
     assertThat(data.getPlugins()).containsOnly(
       entry("java", "4.12.0.11033"), entry("scmgit", "1.2"), entry("other", "undefined"));
@@ -246,22 +245,6 @@ public class TelemetryDataLoaderImplTest {
         tuple(2L, 0L, "undetected", "undetected"));
   }
 
-  @Test
-  public void data_contains_no_license_type_on_community_edition() {
-    TelemetryData data = communityUnderTest.load();
-
-    assertThat(data.getLicenseType()).isEmpty();
-  }
-
-  @Test
-  public void data_contains_no_license_type_on_commercial_edition_if_no_license() {
-    when(licenseReader.read()).thenReturn(Optional.empty());
-
-    TelemetryData data = commercialUnderTest.load();
-
-    assertThat(data.getLicenseType()).isEmpty();
-  }
-
   @Test
   public void data_contains_weekly_count_sonarlint_users() {
     db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000L));
@@ -275,18 +258,6 @@ public class TelemetryDataLoaderImplTest {
       .hasSize(4);
   }
 
-  @Test
-  public void data_has_license_type_on_commercial_edition_if_no_license() {
-    String licenseType = randomAlphabetic(12);
-    LicenseReader.License license = mock(LicenseReader.License.class);
-    when(license.getType()).thenReturn(licenseType);
-    when(licenseReader.read()).thenReturn(Optional.of(license));
-
-    TelemetryData data = commercialUnderTest.load();
-
-    assertThat(data.getLicenseType()).contains(licenseType);
-  }
-
   @Test
   public void send_server_id_and_version() {
     String id = randomAlphanumeric(40);
@@ -316,6 +287,13 @@ public class TelemetryDataLoaderImplTest {
     assertThat(data.getInstallationVersion()).isEqualTo(installationVersion);
   }
 
+  @Test
+  public void send_correct_sequence_number() {
+    internalProperties.write(TelemetryDaemon.I_PROP_MESSAGE_SEQUENCE, "10");
+    TelemetryData data = communityUnderTest.load();
+    assertThat(data.getMessageSequenceNumber()).isEqualTo(11L);
+  }
+
   @Test
   public void do_not_send_server_installation_details_if_missing_property() {
     TelemetryData data = communityUnderTest.load();
diff --git a/sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java b/sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java
new file mode 100644 (file)
index 0000000..542fb53
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.telemetry;
+
+import org.sonar.api.utils.text.JsonWriter;
+
+public interface TelemetryExtension {
+  void write(JsonWriter json);
+}