Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion flow-server/src/main/java/com/vaadin/flow/server/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,21 @@ public final class Constants implements Serializable {
*/
public static final boolean DEFAULT_REQUIRE_HOME_NODE_EXECUTABLE = false;

/**
* The name of the environment variable that controls whether server-side
* usage statistics is enabled.
*
* Usage statistics are disabled if the environment variable is set to
* "false".
*/
public static final String VAADIN_USAGE_STATS_ENABLED = "VAADIN_USAGE_STATS_ENABLED";

/**
* The default value for whether usage statistics is enabled.
*/
public static final boolean DEFAULT_DEVMODE_STATS = true;
public static final boolean DEFAULT_DEVMODE_STATS = !"false"
.equalsIgnoreCase(
System.getenv(Constants.VAADIN_USAGE_STATS_ENABLED));

/**
* Internal parameter which prevent validation for annotations which are
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.server;

import org.junit.Assert;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;

/**
* Unit tests for VAADIN_USAGE_STATS_ENABLED environment variable affecting
* Constants.DEFAULT_DEVMODE_STATS.
*/
public class ConstantsUsageStatsEnvTest {

private String runIsolated(Boolean setEnv, String value)
throws IOException, InterruptedException {
String javaHome = System.getProperty("java.home");
String javaBin = javaHome + java.io.File.separator + "bin"
+ java.io.File.separator + "java";
String classpath = System.getProperty("java.class.path");
ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classpath,
"com.vaadin.flow.server.PrintDefaultDevModeStatsMain");

Map<String, String> env = builder.environment();
if (setEnv == null || !setEnv) {
// Ensure the environment variable is not present
env.remove(Constants.VAADIN_USAGE_STATS_ENABLED);
} else {
env.put(Constants.VAADIN_USAGE_STATS_ENABLED, value);
}

builder.redirectErrorStream(true);
Process process = builder.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
int exit = process.waitFor();
if (exit != 0) {
throw new AssertionError("Child JVM exited with code " + exit
+ "; output: " + line);
}
return line == null ? "" : line.trim();
}
}

@Test
public void whenEnvNotSet_statsEnabledByDefault() throws Exception {
String out = runIsolated(false, null);
Assert.assertEquals("true", out);
}

@Test
public void whenEnvFalse_statsDisabled() throws Exception {
String out = runIsolated(true, "false");
Assert.assertEquals("false", out);
}

@Test
public void whenEnvFALSE_statsDisabled() throws Exception {
String out = runIsolated(true, "FALSE");
Assert.assertEquals("false", out);
}

@Test
public void whenEnvTrue_statsEnabled() throws Exception {
String out = runIsolated(true, "true");
Assert.assertEquals("true", out);
}

@Test
public void whenEnvRandom_statsEnabled() throws Exception {
String out = runIsolated(true, "random-value");
Assert.assertEquals("true", out);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.flow.server;

/**
* Helper main class for testing the value of
* {@link Constants#DEFAULT_DEVMODE_STATS} in an isolated JVM where we can
* control the environment variables.
*/
public class PrintDefaultDevModeStatsMain {

public static void main(String[] args) {
System.out.println(Constants.DEFAULT_DEVMODE_STATS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@
package com.vaadin.base.devserver.stats;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.databind.JsonNode;

import com.vaadin.base.devserver.ServerInfo;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.Version;
import com.vaadin.pro.licensechecker.MachineId;

Expand Down Expand Up @@ -85,6 +90,32 @@ public static DevModeUsageStatistics init(File projectFolder,

getLogger().debug("Telemetry enabled");

final Path statisticDirPath = storage.getUsageStatisticsFile()
.getParentFile().toPath();
final Path firstSeenPath = statisticDirPath
.resolve("telemetry-notice-seen.txt");
if (!Files.exists(firstSeenPath)) {
// Inspired by
// https://learn.microsoft.com/en-us/dotnet/core/tools/telemetry#disclosure
getLogger().info("Telemetry");
getLogger().info("---------");
getLogger().info(
"Vaadin collects usage data in order to help us improve your experience. "
+ "You can opt-out of telemetry by setting the {} environment variable value to 'false'.",
Constants.VAADIN_USAGE_STATS_ENABLED);
getLogger().info(
"Read more about Vaadin telemetry at https://vaadin.com/docs/latest/flow/configuration/development-mode#usage-statistics");

try {
Files.createDirectories(statisticDirPath);
Files.writeString(firstSeenPath, Instant.now().toString());
} catch (IOException ioe) {
getLogger().warn(
"Failed to create telemetry notice first seen file",
ioe);
}
}

storage.access(() -> {
instance = new DevModeUsageStatistics(projectFolder, storage);
// Make sure we are tracking the right project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.base.devserver.stats;

import com.vaadin.flow.testutil.TestUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

/**
* Tests that the telemetry notice is logged on first initialization and
* suppressed on subsequent runs when the notice marker file already exists.
*/
public class DevModeUsageStatisticsLoggingTest {

private Path tempDir;
private File usageStatisticsFile;
private StatisticsStorage storage;
private StatisticsSender sender;

private PrintStream originalErr;
private ByteArrayOutputStream capturedErr;

@Before
public void setUp() throws Exception {
// Create an isolated directory for this test so the notice marker file
// does not collide with other tests
tempDir = Files.createTempDirectory("vaadin-telemetry-test-");
tempDir.toFile().deleteOnExit();

usageStatisticsFile = tempDir.resolve("usage-statistics.json").toFile();
copyStatsTemplate(usageStatisticsFile);

storage = Mockito.spy(new StatisticsStorage());
Mockito.when(storage.getUsageStatisticsFile())
.thenReturn(usageStatisticsFile);

sender = Mockito.spy(new StatisticsSender(storage));
Mockito.doAnswer(inv -> null).when(sender)
.triggerSendIfNeeded(Mockito.any());

// Capture slf4j-simple output which goes to System.err by default
originalErr = System.err;
capturedErr = new ByteArrayOutputStream();
System.setErr(new PrintStream(capturedErr, true,
StandardCharsets.UTF_8.name()));
}

@After
public void tearDown() throws Exception {
System.setErr(originalErr);
// Best-effort cleanup of temp dir
try {
Files.walk(tempDir)
.sorted((a, b) -> b.getNameCount() - a.getNameCount())
.forEach(p -> p.toFile().delete());
} catch (IOException ignore) {
}
}

@Test
public void logsTelemetryNoticeOnlyOnFirstRun() {
// First init should log the telemetry notice since the marker file does
// not exist
DevModeUsageStatistics.init(tempDir.toFile(), storage, sender);
String firstRunLogs = capturedErr.toString(StandardCharsets.UTF_8);
Assert.assertTrue("Expected telemetry notice to be logged on first run",
firstRunLogs.contains(
"Vaadin collects usage data in order to help us improve your experience."));

// Clear captured logs
capturedErr.reset();

// Second init should NOT log the notice since the marker file now
// exists
DevModeUsageStatistics.init(tempDir.toFile(), storage, sender);
String secondRunLogs = capturedErr.toString(StandardCharsets.UTF_8);
Assert.assertFalse(
"Telemetry notice must not be logged again after marker file is created",
secondRunLogs.contains(
"Vaadin collects usage data in order to help us improve your experience."));
}

private static void copyStatsTemplate(File target) throws IOException {
URL res = TestUtils
.getTestResource("stats-data/usage-statistics-1.json");
if (res == null) {
throw new IOException(
"Test resource stats-data/usage-statistics-1.json not found");
}
byte[] bytes = Files.readAllBytes(new File(res.getFile()).toPath());
Files.write(target.toPath(), bytes);
}
}
Loading