Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
c3de681
Extract config impl to separate module
NonSwag Apr 19, 2026
4cd2c0e
Update plugin application code
NonSwag Apr 19, 2026
1f25e80
Refactor config handling
NonSwag Apr 20, 2026
dc8614e
Major metrics schema refactor
NonSwag Apr 20, 2026
6d0c4f8
Simplified metrics construction flow overhead
NonSwag Apr 21, 2026
fddb279
Added injection support for platform context
NonSwag Apr 21, 2026
c412bf9
Update examples to reflect the current best practices
NonSwag Apr 21, 2026
cfb8b8c
Pass the server id to feature flag service
NonSwag Apr 21, 2026
6b5512a
Document SimpleContext constructor
NonSwag Apr 21, 2026
6d35816
Fix happy little accident
NonSwag Apr 21, 2026
db32d9f
Add more test coverage and fixed awful smoke tests
NonSwag Apr 21, 2026
42ab715
Add fabric client support
NonSwag Apr 21, 2026
73a4fe8
Stacktrace fingerprinting
NonSwag Apr 21, 2026
8c2fad3
Rename hash method to hash128 and replace JsonObject with String para…
NonSwag Apr 21, 2026
918eac4
Rename isLibraryClass to isLibraryFrame
NonSwag Apr 21, 2026
cf1ef1c
Integrate stacktrace fingerprinting
NonSwag Apr 21, 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
16 changes: 9 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ val javaVersionsOverride = mapOf(
val defaultJavaVersion = 17

subprojects {
apply(plugin = "java")
apply(plugin = "java-library")
apply {
plugin("java")
plugin("java-library")
}

val example = project.name.startsWith("example")
if (example) {
apply(plugin = "com.gradleup.shadow")
val noPublish = project.name.startsWith("example") || project.name != "config"
if (noPublish) {
apply { plugin("com.gradleup.shadow") }
} else {
apply(plugin = "maven-publish")
apply { plugin("maven-publish") }
}

group = "dev.faststats.metrics"
Expand Down Expand Up @@ -94,7 +96,7 @@ subprojects {
}

afterEvaluate {
if (example) return@afterEvaluate
if (noPublish) return@afterEvaluate
extensions.configure<PublishingExtension> {
publications.create<MavenPublication>("maven") {
artifactId = project.name
Expand Down
1 change: 1 addition & 0 deletions bukkit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ configurations.compileClasspath {

dependencies {
api(project(":core"))
implementation(project(":config"))
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
}
63 changes: 14 additions & 49 deletions bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java
Original file line number Diff line number Diff line change
@@ -1,56 +1,30 @@
package com.example;

import dev.faststats.ErrorTracker;
import dev.faststats.bukkit.BukkitContext;
import dev.faststats.bukkit.BukkitMetrics;
import dev.faststats.core.ErrorTracker;
import dev.faststats.core.data.Metric;
import dev.faststats.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 BukkitContext context = new BukkitContext(this, "YOUR_TOKEN_HERE");

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))
private final BukkitMetrics metrics = context.metrics()
// 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
// #onFlush is invoked after successful metrics submission
// This is useful for cleaning up cached data
.onFlush(() -> gameCount.set(0)) // reset game count on flush

.debug(true) // Enable debug mode for development and testing

.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
.create(this);
.create();

@Override
public void onEnable() {
Expand All @@ -62,15 +36,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
39 changes: 39 additions & 0 deletions bukkit/src/main/java/dev/faststats/bukkit/BukkitContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.faststats.bukkit;

import dev.faststats.SimpleContext;
import dev.faststats.Token;
import dev.faststats.config.SimpleConfig;
import org.bukkit.plugin.Plugin;

import java.nio.file.Path;

/**
* Bukkit FastStats context.
*
* @since 0.23.0
*/
public final class BukkitContext extends SimpleContext {
final Plugin plugin;

public BukkitContext(final Plugin plugin, @Token final String token) {
super(SimpleConfig.read(getConfigPath(plugin)), token);
this.plugin = plugin;
}

@Override
public BukkitMetrics.Factory metrics() {
return new BukkitMetricsImpl.Factory(this);
}

private static Path getConfigPath(final Plugin plugin) {
return getPluginsFolder(plugin).resolve("faststats").resolve("config.properties");
}

private static Path getPluginsFolder(final Plugin plugin) {
try {
return plugin.getServer().getPluginsFolder().toPath();
} catch (final NoSuchMethodError e) {
return plugin.getDataFolder().getParentFile().toPath();
}
}
}
29 changes: 14 additions & 15 deletions bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package dev.faststats.bukkit;

import dev.faststats.core.Metrics;
import dev.faststats.ErrorTracker;
import dev.faststats.Metrics;
import dev.faststats.data.Metric;
import org.bukkit.plugin.IllegalPluginAccessException;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.Contract;

/**
* Bukkit metrics implementation.
*
* @since 0.1.0
*/
public sealed interface BukkitMetrics extends Metrics permits BukkitMetricsImpl {
/**
* Creates a new metrics factory for Bukkit.
*
* @return the metrics factory
* @since 0.1.0
*/
@Contract(pure = true)
static Factory factory() {
return new BukkitMetricsImpl.Factory();
}

/**
* Registers additional exception handlers on Paper-based implementations.
*
Expand All @@ -32,8 +22,17 @@ static Factory factory() {
@Override
void ready() throws IllegalPluginAccessException;

interface Factory extends Metrics.Factory<Plugin, Factory> {
sealed interface Factory extends Metrics.Factory permits BukkitMetricsImpl.Factory {
@Override
Factory addMetric(Metric<?> metric) throws IllegalArgumentException;

@Override
Factory onFlush(Runnable flush);

@Override
Factory errorTracker(ErrorTracker tracker);

@Override
BukkitMetrics create(Plugin object) throws IllegalStateException;
BukkitMetrics create() throws IllegalStateException;
}
}
70 changes: 37 additions & 33 deletions bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package dev.faststats.bukkit;

import com.google.gson.JsonObject;
import dev.faststats.core.SimpleMetrics;
import dev.faststats.ErrorTracker;
import dev.faststats.FastStatsContext;
import dev.faststats.SimpleMetrics;
import dev.faststats.config.SimpleConfig;
import dev.faststats.data.Metric;
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 All @@ -22,8 +23,8 @@ final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics {
@Async.Schedule
@Contract(mutates = "io")
@SuppressWarnings({"deprecation", "Convert2MethodRef"})
private BukkitMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException {
super(factory, config);
private BukkitMetricsImpl(final Factory factory, final Plugin plugin) throws IllegalStateException {
super(factory);

this.plugin = plugin;
final var server = plugin.getServer();
Expand Down Expand Up @@ -63,6 +64,11 @@ private boolean isProxyOnlineMode() {
return settings.getBoolean("bungeecord") && proxies.getBoolean("bungee-cord.online-mode");
}

@Override
protected boolean preSubmissionStart() {
return ((SimpleConfig) getConfig()).preSubmissionStart();
}

@Override
protected void appendDefaultData(final JsonObject metrics) {
metrics.addProperty("minecraft_version", minecraftVersion);
Expand All @@ -76,26 +82,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 All @@ -113,20 +104,33 @@ private <T> Optional<T> tryOrEmpty(final Supplier<T> supplier) {
}
}

static final class Factory extends SimpleMetrics.Factory<Plugin, BukkitMetrics.Factory> implements BukkitMetrics.Factory {
public static final class Factory extends SimpleMetrics.Factory implements BukkitMetrics.Factory {
public Factory(final BukkitContext context) {
super(context);
}

Factory(final FastStatsContext context) {
super(context);
}

@Override
public BukkitMetrics create(final Plugin plugin) throws IllegalStateException {
final var dataFolder = getPluginsFolder(plugin).resolve("faststats");
final var config = dataFolder.resolve("config.properties");
return new BukkitMetricsImpl(this, plugin, config);
public Factory addMetric(final Metric<?> metric) throws IllegalArgumentException {
return (Factory) super.addMetric(metric);
}

private static Path getPluginsFolder(final Plugin plugin) {
try {
return plugin.getServer().getPluginsFolder().toPath();
} catch (final NoSuchMethodError e) {
return plugin.getDataFolder().getParentFile().toPath();
}
@Override
public Factory onFlush(final Runnable flush) {
return (Factory) super.onFlush(flush);
}

@Override
public Factory errorTracker(final ErrorTracker tracker) {
return (Factory) super.errorTracker(tracker);
}

@Override
public BukkitMetrics create() throws IllegalStateException {
return new BukkitMetricsImpl(this, ((BukkitContext) context).plugin);
}
}
}
5 changes: 3 additions & 2 deletions bukkit/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
exports dev.faststats.bukkit;

requires com.google.gson;
requires dev.faststats.core;
requires dev.faststats.config;
requires dev.faststats;
requires java.logging;
requires org.bukkit;

requires static org.jetbrains.annotations;
requires static org.jspecify;
}
}
1 change: 1 addition & 0 deletions bungeecord/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ repositories {

dependencies {
api(project(":core"))
implementation(project(":config"))
compileOnly("net.md-5:bungeecord-api:26.1-R0.1-SNAPSHOT")
}
Loading