diff --git a/core/src/main/java/dev/faststats/core/ErrorTracker.java b/core/src/main/java/dev/faststats/core/ErrorTracker.java index 4cd62c2..06bea1c 100644 --- a/core/src/main/java/dev/faststats/core/ErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/ErrorTracker.java @@ -1,12 +1,15 @@ package dev.faststats.core; -import dev.faststats.core.concurrent.TrackingExecutors; import dev.faststats.core.concurrent.TrackingBase; +import dev.faststats.core.concurrent.TrackingExecutors; import dev.faststats.core.concurrent.TrackingThreadFactory; import dev.faststats.core.concurrent.TrackingThreadPoolExecutor; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; +import java.util.Optional; +import java.util.function.BiConsumer; + /** * An error tracker. * @@ -72,12 +75,34 @@ static ErrorTracker contextUnaware() { /** * Attaches an error context to the tracker. + *

+ * If the class loader is {@code null}, the tracker will track all errors. * * @param loader the class loader * @since 0.10.0 */ void attachErrorContext(@Nullable ClassLoader loader); + /** + * Sets the error event handler which will be called when an error is tracked automatically. + *

+ * The purpose of this handler is to allow custom error handling like logging. + * + * @param errorEvent the error event handler + * @since 0.11.0 + */ + @Contract(mutates = "this") + void setContextErrorHandler(@Nullable BiConsumer<@Nullable ClassLoader, Throwable> errorEvent); + + /** + * Returns the error event handler which will be called when an error is tracked automatically. + * + * @return the error event handler + * @since 0.11.0 + */ + @Contract(pure = true) + Optional> getContextErrorHandler(); + /** * Returns the tracking base. * diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index e0eb365..01eb2d6 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -61,6 +61,8 @@ public interface Metrics { interface Factory { /** * Adds a chart to the metrics submission. + *

+ * If {@link Config#additionalMetrics()} is disabled, the chart will not be submitted. * * @param chart the chart to add * @return the metrics factory @@ -72,6 +74,8 @@ interface Factory { /** * Sets the error tracker for this metrics instance. + *

+ * If {@link Config#errorTracking()} is disabled, no errors will be submitted. * * @param tracker the error tracker * @return the metrics factory @@ -158,16 +162,34 @@ interface Config { * Bypassing this setting may get your project banned from FastStats.
* Users have to be able to opt out from metrics submission. * - * @return true if metrics submission is enabled, false otherwise + * @return {@code true} if metrics submission is enabled, {@code false} otherwise * @since 0.1.0 */ @Contract(pure = true) boolean enabled(); + /** + * Whether error tracking is enabled across all metrics instances. + * + * @return {@code true} if error tracking is enabled, {@code false} otherwise + * @since 0.11.0 + */ + @Contract(pure = true) + boolean errorTracking(); + + /** + * Whether additional metrics are enabled across all metrics instances. + * + * @return {@code true} if additional metrics are enabled, {@code false} otherwise + * @since 0.11.0 + */ + @Contract(pure = true) + boolean additionalMetrics(); + /** * Whether debug logging is enabled across all metrics instances. * - * @return true if debug logging is enabled, false otherwise + * @return {@code true} if debug logging is enabled, {@code false} otherwise * @since 0.1.0 */ @Contract(pure = true) diff --git a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java index bf4c437..f60e3e1 100644 --- a/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java @@ -12,7 +12,9 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; final class SimpleErrorTracker implements ErrorTracker { private final int stackTraceLimit = Math.min(50, Integer.getInteger("faststats.stack-trace-limit", 15)); @@ -24,6 +26,8 @@ final class SimpleErrorTracker implements ErrorTracker { private final TrackingThreadFactory threadFactory = new SimpleTrackingThreadFactory(this); private final TrackingThreadPoolExecutor threadPoolExecutor = new SimpleTrackingThreadPoolExecutor(this); + private @Nullable BiConsumer<@Nullable ClassLoader, Throwable> errorEvent = null; + @Override public void trackError(String message) { trackError(new RuntimeException(message)); @@ -143,10 +147,21 @@ public void attachErrorContext(@Nullable ClassLoader loader) { Thread.setDefaultUncaughtExceptionHandler((thread, error) -> { if (handler != null) handler.uncaughtException(thread, error); if (loader != null && !isSameLoader(loader, error)) return; + if (errorEvent != null) errorEvent.accept(loader, error); trackError(error); }); } + @Override + public void setContextErrorHandler(@Nullable BiConsumer<@Nullable ClassLoader, Throwable> errorEvent) { + this.errorEvent = errorEvent; + } + + @Override + public Optional> getContextErrorHandler() { + return Optional.ofNullable(errorEvent); + } + @Override public TrackingBase base() { return base; diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index a713133..55762a7 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -30,6 +30,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiPredicate; import java.util.zip.GZIPOutputStream; import static java.nio.charset.StandardCharsets.UTF_8; @@ -62,11 +63,11 @@ public abstract class SimpleMetrics implements Metrics { protected SimpleMetrics(Factory factory, Path config) throws IllegalStateException { if (factory.token == null) throw new IllegalStateException("Token must be specified"); - this.charts = Set.copyOf(factory.charts); this.config = new Config(config); + this.charts = this.config.additionalMetrics ? Set.copyOf(factory.charts) : Set.of(); this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || this.config.debug(); this.token = factory.token; - this.tracker = factory.tracker; + this.tracker = this.config.errorTracking ? factory.tracker : null; this.url = factory.url; Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -85,7 +86,7 @@ protected SimpleMetrics(Config config, Set> charts, @Token String token throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); } - this.charts = Set.copyOf(charts); + this.charts = config.additionalMetrics ? Set.copyOf(charts) : Set.of(); this.config = config; this.debug = debug; this.token = token; @@ -339,8 +340,10 @@ public Metrics.Factory url(URI url) { protected static final class Config implements Metrics.Config { private final UUID serverId; + private final boolean additionalMetrics; private final boolean debug; private final boolean enabled; + private final boolean errorTracking; private final boolean firstRun; @Contract(mutates = "io") @@ -359,23 +362,37 @@ protected Config(Path file) { saveConfig.set(true); return UUID.randomUUID(); } - }).orElseGet(UUID::randomUUID); + }).orElseGet(() -> { + saveConfig.set(true); + return UUID.randomUUID(); + }); + + BiPredicate predicate = (key, defaultValue) -> { + return properties.map(object -> object.getProperty(key)).map(Boolean::parseBoolean).orElseGet(() -> { + saveConfig.set(true); + return defaultValue; + }); + }; - this.enabled = properties.map(object -> object.getProperty("enabled")).map(Boolean::parseBoolean).orElse(true); - this.debug = properties.map(object -> object.getProperty("debug")).map(Boolean::parseBoolean).orElse(false); + this.enabled = predicate.test("enabled", true); + this.errorTracking = predicate.test("submitErrors", true); + this.additionalMetrics = predicate.test("submitAdditionalMetrics", true); + this.debug = predicate.test("debug", false); if (saveConfig.get()) try { - save(file, serverId, enabled, debug); + save(file, serverId, enabled, errorTracking, additionalMetrics, debug); } catch (IOException e) { throw new RuntimeException("Failed to save metrics config", e); } } @VisibleForTesting - public Config(UUID serverId, boolean enabled, boolean debug) { + public Config(UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) { this.serverId = serverId; this.enabled = enabled; this.debug = debug; + this.errorTracking = errorTracking; + this.additionalMetrics = additionalMetrics; this.firstRun = false; } @@ -389,6 +406,16 @@ public boolean enabled() { return enabled; } + @Override + public boolean errorTracking() { + return errorTracking; + } + + @Override + public boolean additionalMetrics() { + return additionalMetrics; + } + @Override public boolean debug() { return debug; @@ -405,7 +432,7 @@ private static Optional readOrEmpty(Path file) { } } - private static void save(Path file, UUID serverId, boolean enabled, boolean debug) throws IOException { + private static void save(Path file, UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) throws IOException { Files.createDirectories(file.getParent()); try (var out = Files.newOutputStream(file); var writer = new OutputStreamWriter(out, UTF_8)) { @@ -413,19 +440,24 @@ private static void save(Path file, UUID serverId, boolean enabled, boolean debu properties.setProperty("serverId", serverId.toString()); properties.setProperty("enabled", Boolean.toString(enabled)); + properties.setProperty("submitErrors", Boolean.toString(errorTracking)); + properties.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); properties.setProperty("debug", Boolean.toString(debug)); var comment = """ - FastStats (https://faststats.dev) gathers basic information for plugin developers, - # such as the number of users and total player count. - # Keeping metrics enabled is recommended, but you can disable them if you prefer. - # Enabling metrics does not affect performance, - # and all data sent to FastStats is completely anonymous. - + FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. + # This helps developers understand how their projects are used in the real world. + # + # No IP addresses, player data, or personal information is collected. + # The server ID below is randomly generated and can be regenerated at any time. + # + # Enabling metrics has no noticeable performance impact. + # Keeping metrics enabled is recommended, but you can disable them by setting 'enabled=false'. + # # If you suspect a plugin is collecting personal data or bypassing the "enabled" option, - # please report it to the FastStats team (https://faststats.dev/abuse). - - # For more information, visit https://faststats.dev/info + # please report it at: https://faststats.dev/abuse + # + # For more information, visit: https://faststats.dev/info """; properties.store(writer, comment); } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 377b43e..174b9d6 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -16,7 +16,7 @@ @NullMarked public class MockMetrics extends SimpleMetrics { public MockMetrics(UUID serverId, @Token String token, @Nullable ErrorTracker tracker, boolean debug) { - super(new SimpleMetrics.Config(serverId, true, debug), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); + super(new SimpleMetrics.Config(serverId, true, true, true, debug), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/gradle.properties b/gradle.properties index c1bf17f..c57f47b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.10.1 +version=0.11.0