Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3495bb7
Refactor feature flags
NonSwag Apr 17, 2026
1fc446e
Split metrics and flags url into separate values
NonSwag Apr 19, 2026
6f508d7
Throw on non-finite numbers
NonSwag Apr 19, 2026
adcc65a
Replace Object with JsonPrimitive in Attributes
NonSwag Apr 19, 2026
a62722a
Add Type enum to FeatureFlags
NonSwag Apr 19, 2026
9b06da9
Refactor SimpleFeatureFlagService
NonSwag Apr 19, 2026
7682962
Generalize terms in onboarding message and default config
NonSwag Apr 19, 2026
fc4d8f3
Refactored logging
NonSwag Apr 19, 2026
e5be6b3
Removed settings and ability to define metrics URL and debug
NonSwag Apr 19, 2026
f4f548e
Document FeatureFlags
NonSwag Apr 19, 2026
e983d25
Document Attributes#forEachPrimitive
NonSwag Apr 19, 2026
ba59d49
Use correct url
NonSwag Apr 19, 2026
24786f9
Make SDK properties static
NonSwag Apr 19, 2026
efc352a
Add minimal logger api
NonSwag Apr 19, 2026
aa162de
Extracts constants to its own class
NonSwag Apr 19, 2026
012fc32
Add dedicated Hytale logger
NonSwag Apr 19, 2026
53c997e
Use custom filter predicate
NonSwag Apr 19, 2026
fcc26bd
Move logger below metrics server url
NonSwag Apr 19, 2026
6d23786
Removed unused imports
NonSwag Apr 19, 2026
ee4acd3
Replace Gson#toJson with toString
NonSwag Apr 19, 2026
8955731
Add logger to feature flag service
NonSwag Apr 19, 2026
6be7deb
Decouple config from Metrics interface
NonSwag Apr 19, 2026
dc9c7da
Undo happy little accident :)
NonSwag Apr 19, 2026
aceb872
Add info comments to example
NonSwag Apr 19, 2026
100684f
Throw on negative ttl
NonSwag Apr 19, 2026
dd34a55
Add attributes and TTL getters
NonSwag Apr 19, 2026
32cff12
Refactor URL retrieval
NonSwag Apr 19, 2026
10bec1a
Add `getLogger(Class)` overload
NonSwag Apr 19, 2026
7925c67
Decouple metrics and feature flags
NonSwag Apr 19, 2026
9fb8104
Cancel all running fetches on shutdown
NonSwag Apr 19, 2026
2c8d212
Retrieve server id from config
NonSwag Apr 19, 2026
9ed0502
Unseal config
NonSwag Apr 19, 2026
f4f9a69
Update config comment
NonSwag Apr 19, 2026
95e1aab
Very elegant but sounds stupid
NonSwag Apr 19, 2026
70af53f
Prepare for config impl extraction
NonSwag Apr 19, 2026
f82feb7
todo
NonSwag Apr 19, 2026
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
52 changes: 8 additions & 44 deletions bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,22 @@
import dev.faststats.core.data.Metric;
import org.bukkit.plugin.java.JavaPlugin;

import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.concurrent.atomic.AtomicInteger;

public class ExamplePlugin extends JavaPlugin {
// context-aware error tracker, automatically tracks errors in the same class loader
public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware()
// Ignore specific errors and messages
.ignoreError(InvocationTargetException.class, "Expected .* but got .*") // Ignored an error with a message
.ignoreError(AccessDeniedException.class); // Ignored a specific error type

// context-unaware error tracker, does not automatically track errors
public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware()
// Anonymize error messages if required
.anonymize("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$", "[email hidden]") // Email addresses
.anonymize("Bearer [A-Za-z0-9._~+/=-]+", "Bearer [token hidden]") // Bearer tokens in error messages
.anonymize("AKIA[0-9A-Z]{16}", "[aws-key hidden]") // AWS access key IDs
.anonymize("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "[uuid hidden]") // UUIDs (e.g. session/user IDs)
.anonymize("([?&](?:api_?key|token|secret)=)[^&\\s]+", "$1[redacted]"); // API keys in query strings

public final class ExamplePlugin extends JavaPlugin {
private final AtomicInteger gameCount = new AtomicInteger();

private final BukkitMetrics metrics = BukkitMetrics.factory()
.url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only

// Custom example metrics
// For this to work you have to create a corresponding data source in your project settings first
.addMetric(Metric.number("example_metric", () -> 42))
// Custom metrics require a corresponding data source in your project settings
.addMetric(Metric.number("game_count", gameCount::get))
.addMetric(Metric.string("example_string", () -> "Hello, World!"))
.addMetric(Metric.bool("example_boolean", () -> true))
.addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"}))
.addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3}))
.addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false}))
.addMetric(Metric.string("server_version", () -> "1.0.0"))

// Attach an error tracker
// This must be enabled in the project settings
.errorTracker(ERROR_TRACKER)
// Error tracking must be enabled in the project settings
.errorTracker(ErrorTracker.contextAware())

.onFlush(() -> gameCount.set(0)) // Reset game count on flush

.debug(true) // Enable debug mode for development and testing
// #onFlush is invoked after successful metrics submission
// This is useful for cleaning up cached data
.onFlush(() -> gameCount.set(0)) // reset game count on flush

.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
.create(this);
Expand All @@ -62,15 +35,6 @@ public void onDisable() {
metrics.shutdown(); // safely shut down metrics submission
}

public void doSomethingWrong() {
try {
// Do something that might throw an error
throw new RuntimeException("Something went wrong!");
} catch (final Exception e) {
CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e);
}
}

public void startGame() {
gameCount.incrementAndGet();
}
Expand Down
19 changes: 1 addition & 18 deletions bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Async;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.logging.Level;

final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics {
private final Plugin plugin;
Expand Down Expand Up @@ -76,26 +74,11 @@ private int getPlayerCount() {
try {
return plugin.getServer().getOnlinePlayers().size();
} catch (final Throwable t) {
error("Failed to get player count", t);
logger.error("Failed to get player count", t);
return 0;
}
}

@Override
protected void printError(final String message, @Nullable final Throwable throwable) {
plugin.getLogger().log(Level.SEVERE, message, throwable);
}

@Override
protected void printInfo(final String message) {
plugin.getLogger().info(message);
}

@Override
protected void printWarning(final String message) {
plugin.getLogger().warning(message);
}

@Override
public void ready() {
if (getErrorTracker().isPresent()) try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,22 @@
import dev.faststats.core.data.Metric;
import net.md_5.bungee.api.plugin.Plugin;

import java.net.URI;
import java.util.concurrent.atomic.AtomicInteger;

public class ExamplePlugin extends Plugin {
// context-aware error tracker, automatically tracks errors in the same class loader
public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware();

// context-unaware error tracker, does not automatically track errors
public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware();
private final AtomicInteger gameCount = new AtomicInteger();

private final Metrics metrics = BungeeMetrics.factory()
.url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only

// Custom example metrics
// For this to work you have to create a corresponding data source in your project settings first
.addMetric(Metric.number("example_metric", () -> 42))
.addMetric(Metric.string("example_string", () -> "Hello, World!"))
.addMetric(Metric.bool("example_boolean", () -> true))
.addMetric(Metric.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"}))
.addMetric(Metric.numberArray("example_number_array", () -> new Number[]{1, 2, 3}))
.addMetric(Metric.booleanArray("example_boolean_array", () -> new Boolean[]{true, false}))
// Custom metrics require a corresponding data source in your project settings
.addMetric(Metric.number("game_count", gameCount::get))
.addMetric(Metric.string("server_version", () -> "1.0.0"))

// Attach an error tracker
// This must be enabled in the project settings
.errorTracker(ERROR_TRACKER)
// Error tracking must be enabled in the project settings
.errorTracker(ErrorTracker.contextAware())

.debug(true) // Enable debug mode for development and testing
// #onFlush is invoked after successful metrics submission
// This is useful for cleaning up cached data
.onFlush(() -> gameCount.set(0)) // reset game count on flush

.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
.create(this);
Expand All @@ -41,12 +31,7 @@ public void onDisable() {
metrics.shutdown(); // safely shut down metrics submission
}

public void doSomethingWrong() {
try {
// Do something that might throw an error
throw new RuntimeException("Something went wrong!");
} catch (final Exception e) {
CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e);
}
public void startGame() {
gameCount.incrementAndGet();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@
import net.md_5.bungee.api.plugin.Plugin;
import org.jetbrains.annotations.Async;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;

final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics {
private final Logger logger;
private final ProxyServer server;
private final Plugin plugin;

Expand All @@ -23,7 +19,6 @@ final class BungeeMetricsImpl extends SimpleMetrics implements BungeeMetrics {
private BungeeMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException {
super(factory, config);

this.logger = plugin.getLogger();
this.server = plugin.getProxy();
this.plugin = plugin;

Expand All @@ -39,21 +34,6 @@ protected void appendDefaultData(final JsonObject metrics) {
metrics.addProperty("server_type", server.getName());
}

@Override
protected void printError(final String message, @Nullable final Throwable throwable) {
logger.log(Level.SEVERE, message, throwable);
}

@Override
protected void printInfo(final String message) {
logger.info(message);
}

@Override
protected void printWarning(final String message) {
logger.warning(message);
}

static final class Factory extends SimpleMetrics.Factory<Plugin, BungeeMetrics.Factory> implements BungeeMetrics.Factory {
@Override
public Metrics create(final Plugin plugin) throws IllegalStateException {
Expand Down
3 changes: 3 additions & 0 deletions core/example/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation(project(":core"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.faststats.example;

import dev.faststats.core.ErrorTracker;

import java.lang.reflect.InvocationTargetException;
import java.nio.file.AccessDeniedException;

public final class ErrorTrackerExample {
// Context-aware: automatically tracks uncaught errors from the same class loader
public static final ErrorTracker CONTEXT_AWARE = ErrorTracker.contextAware()
.ignoreError(InvocationTargetException.class, "Expected .* but got .*")
.ignoreError(AccessDeniedException.class);

// Context-unaware: only tracks errors passed to trackError() manually
public static final ErrorTracker CONTEXT_UNAWARE = ErrorTracker.contextUnaware()
.anonymize("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$", "[email hidden]")
.anonymize("Bearer [A-Za-z0-9._~+/=-]+", "Bearer [token hidden]")
.anonymize("AKIA[0-9A-Z]{16}", "[aws-key hidden]")
.anonymize("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "[uuid hidden]")
.anonymize("([?&](?:api_?key|token|secret)=)[^&\\s]+", "$1[redacted]");

public static void manualTracking() {
try {
throw new RuntimeException("Something went wrong!");
} catch (final Exception e) {
CONTEXT_UNAWARE.trackError(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package dev.faststats.example;

import dev.faststats.core.flags.Attributes;
import dev.faststats.core.flags.FeatureFlag;
import dev.faststats.core.flags.FeatureFlagService;

import java.time.Duration;
import java.time.Instant;

public final class FeatureFlagExample {
public static final FeatureFlagService SERVICE = FeatureFlagService.create(
"YOUR_TOKEN_HERE", // token can be found in the settings of your project
Attributes.create() // Define global attributes
.put("version", "1.2.3")
.put("java_version", System.getProperty("java.version"))
.put("java_vendor", System.getProperty("java.vendor")),
Duration.ofMinutes(10) // Custom cache TTL for resolved flag values
);

// Define flags with default values
public static final FeatureFlag<Boolean> NEW_COMMANDS = SERVICE.define("new_commands", false);
public static final FeatureFlag<String> COMPRESSION = SERVICE.define("compression", "zstd");

public static void usage() {
// Async: waits for the server value to be fetched
NEW_COMMANDS.whenReady().thenAccept(enabled -> {
if (enabled) {
// register new commands
}
});

// Non-blocking: returns the cached value if present without triggering a fetch
COMPRESSION.getCached().ifPresent(compression -> {
switch (compression) {
case "zstd":
// default compression
break;
case "lz4":
// experimental compression
break;
default:
break;
}
});

// Refresh stale values explicitly when your code decides it is needed
if (COMPRESSION.getExpiration().filter(Instant.now()::isAfter).isPresent()) {
COMPRESSION.fetch();
}

// Opt-in/out (requires allow_specific_opt_in on server)
NEW_COMMANDS.optIn().thenAccept(updatedValue -> {
if (updatedValue) {
// react to the updated server value
}
});
NEW_COMMANDS.optOut().thenAccept(updatedValue -> {
if (!updatedValue) {
// react to the updated server value
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.faststats.example;

import dev.faststats.core.data.Metric;

public final class MetricTypesExample {
// Single value metrics
public static final Metric<Number> PLAYER_COUNT = Metric.number("player_count", () -> 42);
public static final Metric<String> SERVER_VERSION = Metric.string("server_version", () -> "1.0.0");
public static final Metric<Boolean> MAINTENANCE_MODE = Metric.bool("maintenance_mode", () -> false);

// Array metrics
public static final Metric<String[]> INSTALLED_PLUGINS = Metric.stringArray("installed_plugins", () -> new String[]{"WorldEdit", "Essentials"});
public static final Metric<String[]> WORLDS = Metric.stringArray("worlds", () -> new String[]{"city", "farmworld", "farmworld_nether", "famrworld_end"});
}
60 changes: 60 additions & 0 deletions core/src/main/java/dev/faststats/core/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dev.faststats.core;

import org.jetbrains.annotations.Contract;

import java.util.UUID;

/**
* A representation of the metrics configuration.
*
* @since 0.23.0
*/
public interface Config {
/**
* The server id.
*
* @return the server id
* @since 0.23.0
*/
@Contract(pure = true)
UUID serverId();

/**
* Whether metrics submission is enabled.
* <p>
* <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 {@code true} if metrics submission is enabled, {@code false} otherwise
* @since 0.23.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.23.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.23.0
*/
@Contract(pure = true)
boolean additionalMetrics();

/**
* Whether debug logging is enabled across all metrics instances.
*
* @return {@code true} if debug logging is enabled, {@code false} otherwise
* @since 0.23.0
*/
@Contract(pure = true)
boolean debug();
}
Loading