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 + } + } + } + }"""); + } + }