Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 26 additions & 1 deletion core/src/main/java/dev/faststats/core/ErrorTracker.java
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -72,12 +75,34 @@ static ErrorTracker contextUnaware() {

/**
* Attaches an error context to the tracker.
* <p>
* 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.
* <p>
* 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<BiConsumer<@Nullable ClassLoader, Throwable>> getContextErrorHandler();

/**
* Returns the tracking base.
*
Expand Down
26 changes: 24 additions & 2 deletions core/src/main/java/dev/faststats/core/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public interface Metrics {
interface Factory<T> {
/**
* Adds a chart to the metrics submission.
* <p>
* If {@link Config#additionalMetrics()} is disabled, the chart will not be submitted.
*
* @param chart the chart to add
* @return the metrics factory
Expand All @@ -72,6 +74,8 @@ interface Factory<T> {

/**
* Sets the error tracker for this metrics instance.
* <p>
* If {@link Config#errorTracking()} is disabled, no errors will be submitted.
*
* @param tracker the error tracker
* @return the metrics factory
Expand Down Expand Up @@ -158,16 +162,34 @@ interface Config {
* <b>Bypassing this setting may get your project banned from FastStats.</b><br>
* <b>Users have to be able to opt out from metrics submission.</b>
*
* @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)
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/dev/faststats/core/SimpleErrorTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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<BiConsumer<@Nullable ClassLoader, Throwable>> getContextErrorHandler() {
return Optional.ofNullable(errorEvent);
}

@Override
public TrackingBase base() {
return base;
Expand Down
68 changes: 50 additions & 18 deletions core/src/main/java/dev/faststats/core/SimpleMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(() -> {
Expand All @@ -85,7 +86,7 @@ protected SimpleMetrics(Config config, Set<Chart<?>> 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;
Expand Down Expand Up @@ -339,8 +340,10 @@ public Metrics.Factory<T> 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")
Expand All @@ -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<String, Boolean> 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;
}

Expand All @@ -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;
Expand All @@ -405,27 +432,32 @@ private static Optional<Properties> 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)) {
var properties = new Properties();

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);
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/java/dev/faststats/MockMetrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=0.10.1
version=0.11.0
Loading