diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/UsageStatisticsExporter.java b/flow-server/src/main/java/com/vaadin/flow/internal/UsageStatisticsExporter.java
index 42e8db0b1f5..e0abac56992 100644
--- a/flow-server/src/main/java/com/vaadin/flow/internal/UsageStatisticsExporter.java
+++ b/flow-server/src/main/java/com/vaadin/flow/internal/UsageStatisticsExporter.java
@@ -17,13 +17,9 @@
package com.vaadin.flow.internal;
import java.io.Serializable;
-import java.util.stream.Collectors;
import org.jsoup.nodes.Document;
-import elemental.json.Json;
-import elemental.json.JsonObject;
-
/**
* A class for exporting {@link UsageStatistics} entries.
*
@@ -31,7 +27,10 @@
*
* @author Vaadin Ltd
* @since 3.0
+ * @deprecated server side statistics should not be sent to the client. Will be
+ * removed without replacement.
*/
+@Deprecated(since = "24.5", forRemoval = true)
public class UsageStatisticsExporter implements Serializable {
/**
@@ -40,29 +39,14 @@ public class UsageStatisticsExporter implements Serializable {
*
* @param document
* the document where the statistic entries to be exported to.
+ * @deprecated server side statistics should not be sent to the client. The
+ * method throws an exception if called. Will be removed without
+ * replacement.
*/
+ @Deprecated
public static void exportUsageStatisticsToDocument(Document document) {
- String entries = UsageStatistics.getEntries()
- .map(UsageStatisticsExporter::createUsageStatisticsJson)
- .collect(Collectors.joining(","));
-
- if (!entries.isEmpty()) {
- // Registers the entries in a way that is picked up as a Vaadin
- // WebComponent by the usage stats gatherer
- String builder = "window.Vaadin = window.Vaadin || {};\n"
- + "window.Vaadin.registrations = window.Vaadin.registrations || [];\n"
- + "window.Vaadin.registrations.push(" + entries + ");";
- document.body().appendElement("script").text(builder);
- }
+ throw new UnsupportedOperationException(
+ "Server side usage statistics must not be exported to the client");
}
- private static String createUsageStatisticsJson(
- UsageStatistics.UsageEntry entry) {
- JsonObject json = Json.createObject();
-
- json.put("is", entry.getName());
- json.put("version", entry.getVersion());
-
- return json.toJson();
- }
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
index afaa41f9c03..a13301bcb06 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java
@@ -749,8 +749,6 @@ public Document getBootstrapPage(BootstrapContext context) {
document.body(), targets));
if (!config.isProductionMode()) {
- UsageStatisticsExporter
- .exportUsageStatisticsToDocument(document);
IndexHtmlRequestHandler.addLicenseChecker(document);
}
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
index 36e3bfe1127..4ad5e716340 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java
@@ -145,11 +145,6 @@ public boolean synchronizedHandleRequest(VaadinSession session,
VaadinContext context = session.getService().getContext();
AppShellRegistry registry = AppShellRegistry.getInstance(context);
- if (!config.isProductionMode()) {
- UsageStatisticsExporter
- .exportUsageStatisticsToDocument(indexDocument);
- }
-
// modify the page based on the @PWA annotation
setupPwa(indexDocument, session.getService());
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandler.java
index 9f02d5d6517..62f0cec3f5c 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandler.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandler.java
@@ -217,22 +217,6 @@ protected static String getServiceUrl(VaadinRequest vaadinRequest) {
return BootstrapHandlerHelper.getServiceUrl(vaadinRequest);
}
- private JsonObject getStats() {
- JsonObject stats = Json.createObject();
- UsageStatistics.getEntries().forEach(entry -> {
- String name = entry.getName();
- String version = entry.getVersion();
-
- JsonObject json = Json.createObject();
- json.put("is", name);
- json.put("version", version);
-
- String escapedName = Json.create(name).toJson();
- stats.put(escapedName, json);
- });
- return stats;
- }
-
private JsonValue getErrors(VaadinService service) {
JsonObject errors = Json.createObject();
Optional devModeHandler = DevModeHandlerManager
@@ -287,9 +271,6 @@ protected JsonObject getInitialJson(VaadinRequest request,
if (context.getPushMode().isEnabled()) {
initial.put("pushScript", getPushScript(context));
}
- if (!session.getConfiguration().isProductionMode()) {
- initial.put("stats", getStats());
- }
initial.put("errors", getErrors(request.getService()));
return initial;
diff --git a/flow-server/src/test/java/com/vaadin/flow/internal/UsageStatisticsExporterTest.java b/flow-server/src/test/java/com/vaadin/flow/internal/UsageStatisticsExporterTest.java
deleted file mode 100644
index 0f2902005f4..00000000000
--- a/flow-server/src/test/java/com/vaadin/flow/internal/UsageStatisticsExporterTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.vaadin.flow.internal;
-
-import java.util.stream.Collectors;
-
-import org.jsoup.internal.StringUtil;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-import org.junit.Test;
-
-import elemental.json.Json;
-import elemental.json.JsonObject;
-
-import static org.junit.Assert.assertEquals;
-
-public class UsageStatisticsExporterTest {
-
- @Test
- public void should_append_script_element_to_the_body() {
- Document document = new Document("");
- Element html = document.appendElement("html");
- html.appendElement("body");
-
- UsageStatisticsExporter.exportUsageStatisticsToDocument(document);
-
- String entries = UsageStatistics.getEntries().map(entry -> {
- JsonObject json = Json.createObject();
-
- json.put("is", entry.getName());
- json.put("version", entry.getVersion());
-
- return json.toString();
- }).collect(Collectors.joining(","));
-
- String expected = StringUtil
- .normaliseWhitespace("window.Vaadin = window.Vaadin || {};\n"
- + "window.Vaadin.registrations = window.Vaadin.registrations || [];\n"
- + "window.Vaadin.registrations.push(" + entries + ");");
-
- Elements bodyInlineElements = document.body()
- .getElementsByTag("script");
- String htmlContent = bodyInlineElements.get(0).childNode(0).outerHtml();
- htmlContent = htmlContent.replace("\r", "");
- htmlContent = htmlContent.replace("\n", " ");
- assertEquals(StringUtil.normaliseWhitespace(expected), htmlContent);
- }
-}
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
index 17223323af1..a442b3f4f28 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java
@@ -681,60 +681,12 @@ public void should_add_elements_when_appShellWithConfigurator()
Elements bodyInlineElements = document.body()
.getElementsByTag("script");
// "
// "
- assertEquals(2, bodyInlineElements.size());
- }
-
- @Test
- public void should_export_usage_statistics_in_development_mode()
- throws IOException {
- File projectRootFolder = temporaryFolder.newFolder();
- TestUtil.createIndexHtmlStub(projectRootFolder);
- TestUtil.createStatsJsonStub(projectRootFolder);
- deploymentConfiguration.setProductionMode(false);
- deploymentConfiguration.setProjectFolder(projectRootFolder);
- VaadinServletRequest request = createVaadinRequest("/");
- Mockito.when(request.getHttpServletRequest().getRemoteAddr())
- .thenReturn("127.0.0.1");
- indexHtmlRequestHandler.synchronizedHandleRequest(session, request,
- response);
-
- String indexHtml = responseOutput.toString(StandardCharsets.UTF_8);
- Document document = Jsoup.parse(indexHtml);
-
- Elements bodyInlineElements = document.body()
- .getElementsByTag("script");
- //
assertEquals(1, bodyInlineElements.size());
-
- String entries = UsageStatistics.getEntries().map(entry -> {
- JsonObject json = Json.createObject();
-
- json.put("is", entry.getName());
- json.put("version", entry.getVersion());
-
- return json.toString();
- }).collect(Collectors.joining(","));
-
- String expected = StringUtil
- .normaliseWhitespace("window.Vaadin = window.Vaadin || {}; "
- + "window.Vaadin.registrations = window.Vaadin.registrations || [];\n"
- + "window.Vaadin.registrations.push(" + entries + ");");
-
- assertTrue(isTokenPresent(indexHtml));
-
- String htmlContent = bodyInlineElements.get(0).childNode(0).outerHtml();
- htmlContent = htmlContent.replace("\r", "");
- htmlContent = htmlContent.replace("\n", " ");
- assertEquals(StringUtil.normaliseWhitespace(expected), htmlContent);
}
// Regular expression to match a UUID in the format 8-4-4-4-12
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java
index 5c3a0e80bfd..6af87a2977b 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java
@@ -102,7 +102,6 @@ public void should_produceValidJsonResponse() throws Exception {
JsonObject json = Json.parse(response.getPayload());
- Assert.assertTrue(json.hasKey("stats"));
Assert.assertTrue(json.hasKey("errors"));
Assert.assertTrue(json.hasKey("appConfig"));
Assert.assertTrue(json.getObject("appConfig").hasKey("appId"));
diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java
index 6764c3e9d6a..d5ca067d899 100644
--- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java
+++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java
@@ -186,6 +186,7 @@ public void onConnect(AtmosphereResource resource) {
if (DevToolsToken.getToken()
.equals(resource.getRequest().getParameter("token"))) {
handleConnect(resource);
+ DevModeUsageStatistics.handleServerData();
} else {
getLogger().debug(
"Connection denied because of a missing or invalid token. Either the host is not on the 'vaadin.devmode.hosts-allowed' list or it is using an outdated token");
@@ -243,6 +244,7 @@ public void onDisconnect(AtmosphereResource resource) {
"Push connection {} is not a live-reload connection or already closed",
uuid);
}
+ DevModeUsageStatistics.handleServerData();
}
@Override
diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/DevModeUsageStatistics.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/DevModeUsageStatistics.java
index 9b8696c6a49..e9f1738318b 100644
--- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/DevModeUsageStatistics.java
+++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/DevModeUsageStatistics.java
@@ -17,12 +17,15 @@
package com.vaadin.base.devserver.stats;
import java.io.File;
+import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vaadin.base.devserver.ServerInfo;
+import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.server.Version;
import com.vaadin.pro.licensechecker.MachineId;
@@ -156,7 +159,21 @@ public static void handleBrowserData(JsonObject data) {
.readTree(json);
if (clientData != null && clientData.isObject()) {
clientData.fields().forEachRemaining(
- e -> project.setValue(e.getKey(), e.getValue()));
+ e -> project.computeValue(e.getKey(), jsonNode -> {
+ ObjectNode container;
+ if (jsonNode == null || !jsonNode.isObject()) {
+ container = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ } else {
+ container = (ObjectNode) jsonNode;
+ }
+ // Replace exising entries with data coming from
+ // the browser, preserving server side entries
+ if (e.getValue() instanceof ObjectNode newData) {
+ container.setAll(newData);
+ }
+ return container;
+ }));
}
} catch (Exception e) {
getLogger().debug("Failed to update client telemetry data", e);
@@ -165,6 +182,50 @@ public static void handleBrowserData(JsonObject data) {
}
+ /**
+ * Stores telemetry data collected on the server.
+ */
+ public static void handleServerData() {
+ if (!isStatisticsEnabled()) {
+ return;
+ }
+ getLogger().debug("Persisting server side usage statistics");
+ List entries = UsageStatistics.getEntries()
+ .toList();
+ get().storage.update((global, project) -> {
+ try {
+ project.computeValue("elements", jsonNode -> {
+ ObjectNode elements;
+ if (jsonNode == null || !jsonNode.isObject()) {
+ elements = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ } else {
+ elements = (ObjectNode) jsonNode;
+ }
+ for (UsageStatistics.UsageEntry entry : entries) {
+ if (elements.get(entry
+ .getName()) instanceof ObjectNode jsonEntry) {
+ jsonEntry.put("lastUsed",
+ System.currentTimeMillis());
+ } else {
+ ObjectNode jsonEntry = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ jsonEntry.put("version", entry.getVersion());
+ jsonEntry.put("firstUsed",
+ System.currentTimeMillis());
+ elements.set(entry.getName(), jsonEntry);
+ }
+ }
+ return elements;
+ });
+ } catch (Exception e) {
+ getLogger().debug(
+ "Failed to update server side usage statistics data",
+ e);
+ }
+ });
+ }
+
/**
* Checks if usage statistic collection is currently enabled.
*
diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsContainer.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsContainer.java
index 124dab71c41..48447f70fe1 100644
--- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsContainer.java
+++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsContainer.java
@@ -15,6 +15,8 @@
*/
package com.vaadin.base.devserver.stats;
+import java.util.function.UnaryOperator;
+
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -166,4 +168,21 @@ public double getValueAsDouble(String name) {
return 0.0;
}
+ /**
+ * Stores or updates a JSON object using the given field name.
+ *
+ *
+ * The mapping function is given the existing JSON object, or
+ * {@literal null} if field is not yet present in the storage.
+ *
+ * @param name
+ * name of the field to set or update
+ * @param mappingFunction
+ * the mapping function to compute a value
+ */
+ public void computeValue(String name,
+ UnaryOperator mappingFunction) {
+ json.set(name, mappingFunction.apply(json.get(name)));
+ }
+
}
diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsStorage.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsStorage.java
index ee61d943773..e8b9a42fe8a 100644
--- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsStorage.java
+++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/stats/StatisticsStorage.java
@@ -18,6 +18,7 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.FileSystems;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/stats/DevModeUsageStatisticsTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/stats/DevModeUsageStatisticsTest.java
index f6908209f6c..cdab13b5791 100644
--- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/stats/DevModeUsageStatisticsTest.java
+++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/stats/DevModeUsageStatisticsTest.java
@@ -19,21 +19,32 @@
import java.io.File;
import java.nio.charset.StandardCharsets;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.vaadin.flow.testutil.TestUtils;
-
-import com.vaadin.pro.licensechecker.MachineId;
+import net.jcip.annotations.NotThreadSafe;
import org.apache.commons.io.IOUtils;
+import org.junit.After;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
+import com.vaadin.flow.internal.UsageStatistics;
+import com.vaadin.flow.testutil.TestUtils;
+import com.vaadin.pro.licensechecker.MachineId;
+
import elemental.json.Json;
import elemental.json.JsonObject;
-import net.jcip.annotations.NotThreadSafe;
@NotThreadSafe
public class DevModeUsageStatisticsTest extends AbstractStatisticsTest {
+ @Before
+ @After
+ public void clearUsageStatistic() throws Exception {
+ UsageStatistics.resetEntries();
+ }
+
@Test
public void clientData() throws Exception {
// Init using test project
@@ -284,6 +295,176 @@ public void machineId() throws Exception {
project.get(StatisticsConstants.FIELD_MACHINE_ID).asText());
}
+ @Test
+ public void handleServerData_createEntriesForServerSideStatistic() {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+
+ long before = System.currentTimeMillis();
+ UsageStatistics.markAsUsed("test-has-flow-feature", "99.99.99");
+ DevModeUsageStatistics.handleServerData();
+ long after = System.currentTimeMillis();
+
+ final ObjectNode project = storage.readProject();
+ JsonNode featureNode = assertThatProjectHasFeature(project,
+ "test-has-flow-feature", "99.99.99");
+ Assert.assertNotNull("firstUsed", featureNode.get("firstUsed"));
+ long firstUsed = featureNode.get("firstUsed").longValue();
+ Assert.assertTrue("firstUsed ",
+ firstUsed >= before && firstUsed <= after);
+ Assert.assertNull("lastUsed", featureNode.get("lastUsed"));
+ }
+
+ @Test
+ public void handleServerData_updatesServerSideStatistic() {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+ ObjectNode existingFeatureNode = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ existingFeatureNode.put("version", "99.99.99");
+ long firstUsed = System.currentTimeMillis() - 10000;
+ existingFeatureNode.put("firstUsed", firstUsed);
+
+ storage.update((global, project) -> {
+ ObjectNode elements = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ elements.set("test-has-flow-feature", existingFeatureNode);
+ project.setValue("elements", elements);
+ });
+
+ long before = System.currentTimeMillis();
+ UsageStatistics.markAsUsed("test-has-flow-feature", "99.99.99");
+ DevModeUsageStatistics.handleServerData();
+ long after = System.currentTimeMillis();
+
+ final ObjectNode project = storage.readProject();
+ JsonNode featureNode = assertThatProjectHasFeature(project,
+ "test-has-flow-feature", "99.99.99");
+ Assert.assertEquals("firstUsed", firstUsed,
+ featureNode.get("firstUsed").longValue());
+ long lastUsed = featureNode.get("lastUsed").longValue();
+ Assert.assertTrue("lastUsed ", lastUsed >= before && lastUsed <= after);
+ }
+
+ @Test
+ public void handleServerData_preserveExistingFields() {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+
+ storage.update((global, project) -> {
+ ObjectNode elements = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ elements.set("test-existing-feature",
+ JsonHelpers.getJsonMapper().createObjectNode());
+ project.setValue("elements", elements);
+ });
+
+ UsageStatistics.markAsUsed("test-has-flow-feature", "99.99.99");
+ DevModeUsageStatistics.handleServerData();
+
+ final ObjectNode project = storage.readProject();
+ JsonNode elements = project.get("elements");
+ Assert.assertNotNull("elements key missing", elements);
+ Assert.assertTrue("existing feature missing",
+ elements.has("test-existing-feature"));
+ Assert.assertTrue("existing feature missing",
+ elements.has("test-has-flow-feature"));
+ }
+
+ @Test
+ public void handleBrowserData_createEntriesForBrowserStatistic() {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+
+ JsonObject browserData = createBrowserData();
+ DevModeUsageStatistics.handleBrowserData(browserData);
+
+ final ObjectNode project = storage.readProject();
+ assertThatProjectHasFeature(project, "@vaadin/router", "1.7.4");
+ assertThatProjectHasFeature(project, "@vaadin/common-frontend",
+ "0.0.18");
+ assertThatProjectHasFeature(project, "vaadin-iconset", "99.99.99");
+ Assert.assertEquals("Unexpected number of elements entries", 3,
+ project.get("elements").size());
+
+ assertThatProjectHasFramework(project, "React", "unknown");
+ assertThatProjectHasFramework(project, "Flow", "99.99.99");
+ Assert.assertEquals("Unexpected number of frameworks entries", 2,
+ project.get("frameworks").size());
+
+ assertThatProjectHasTheme(project, "Lumo", "99.99.99");
+ Assert.assertEquals("Unexpected number of themes entries", 1,
+ project.get("themes").size());
+ }
+
+ @Test
+ public void handleBrowserData_overwritesBrowseStatistic()
+ throws JsonProcessingException {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+
+ JsonObject browserData = createBrowserData();
+
+ ObjectNode existingFeatureNode = JsonHelpers.getJsonMapper().readValue(
+ browserData.getObject("browserData").getObject("elements")
+ .getObject("@vaadin/router").toJson(),
+ ObjectNode.class);
+ long firstUsedFromBrowserData = existingFeatureNode.get("firstUsed")
+ .longValue();
+ long firstUsed = firstUsedFromBrowserData - 10000;
+ existingFeatureNode.put("version", "1.0.0");
+ existingFeatureNode.put("firstUsed", firstUsed);
+
+ storage.update((global, project) -> {
+ ObjectNode elements = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ elements.set("@vaadin/router", existingFeatureNode);
+ project.setValue("elements", elements);
+ });
+ ObjectNode project = storage.readProject();
+ assertThatProjectHasFeature(project, "@vaadin/router",
+ existingFeatureNode.get("version").asText());
+
+ DevModeUsageStatistics.handleBrowserData(browserData);
+
+ project = storage.readProject();
+ JsonNode featureNode = assertThatProjectHasFeature(project,
+ "@vaadin/router", "1.7.4");
+ Assert.assertEquals("firstUsed", firstUsedFromBrowserData,
+ featureNode.get("firstUsed").longValue());
+ }
+
+ @Test
+ public void handleBrowserData_preserveExistingFields() {
+ File mavenProjectFolder = TestUtils
+ .getTestFolder("stats-data/maven-project-folder1");
+ DevModeUsageStatistics.init(mavenProjectFolder, storage, sender);
+
+ ObjectNode featureNode = JsonHelpers.getJsonMapper().createObjectNode();
+ featureNode.put("version", "99.99.99");
+ featureNode.put("firstUsed", System.currentTimeMillis() - 90000);
+ featureNode.put("lastUsed", System.currentTimeMillis() - 10000);
+ storage.update((global, project) -> {
+ ObjectNode elements = JsonHelpers.getJsonMapper()
+ .createObjectNode();
+ elements.set("test-existing-feature", featureNode);
+ project.setValue("elements", elements);
+ });
+
+ JsonObject browserData = createBrowserData();
+ DevModeUsageStatistics.handleBrowserData(browserData);
+
+ final ObjectNode project = storage.readProject();
+ assertThatProjectHasFeature(project, "test-existing-feature",
+ "99.99.99");
+ assertThatProjectHasFeature(project, "@vaadin/router", "1.7.4");
+ }
+
private static JsonObject wrapStats(String data) {
JsonObject wrapped = Json.createObject();
wrapped.put("browserData", data);
@@ -297,4 +478,75 @@ private static int getNumberOfProjects(ObjectNode allData) {
return 0;
}
+ private JsonNode assertProjectEntry(ObjectNode project, String container,
+ String name, String version) {
+ Assert.assertTrue(container + " key missing", project.has(container));
+ JsonNode featureNode = project.get(container).get(name);
+ Assert.assertNotNull("entry " + name + " not found in " + container,
+ featureNode);
+ Assert.assertEquals("version", version,
+ featureNode.get("version").asText());
+ Assert.assertNotNull("firstUsed", featureNode.get("firstUsed"));
+ return featureNode;
+ }
+
+ private JsonNode assertThatProjectHasFramework(ObjectNode project,
+ String name, String version) {
+ return assertProjectEntry(project, "frameworks", name, version);
+ }
+
+ private JsonNode assertThatProjectHasTheme(ObjectNode project, String name,
+ String version) {
+ return assertProjectEntry(project, "themes", name, version);
+ }
+
+ private JsonNode assertThatProjectHasFeature(ObjectNode project,
+ String name, String version) {
+ return assertProjectEntry(project, "elements", name, version);
+ }
+
+ private static JsonObject createBrowserData() {
+ return Json.parse("""
+ {
+ "browserData": {
+ "elements": {
+ "@vaadin/router": {
+ "firstUsed": 1655485266038,
+ "version": "1.7.4",
+ "lastUsed": 1717309299243
+ },
+ "@vaadin/common-frontend": {
+ "firstUsed": 1655485266038,
+ "version": "0.0.18",
+ "lastUsed": 1725870127515
+ },
+ "vaadin-iconset": {
+ "firstUsed": 1655485266038,
+ "version": "99.99.99",
+ "lastUsed": 1725870127515
+ }
+ },
+ "frameworks": {
+ "React": {
+ "firstUsed": 1655485266038,
+ "version": "unknown",
+ "lastUsed": 1703682895162
+ },
+ "Flow": {
+ "firstUsed": 1655485266038,
+ "version": "99.99.99",
+ "lastUsed": 1725870127515
+ }
+ },
+ "themes": {
+ "Lumo": {
+ "firstUsed": 1655485266038,
+ "version": "99.99.99",
+ "lastUsed": 1725870127515
+ }
+ }
+ }
+ }""");
+ }
+
}