Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
fa3b284
Refactor feature flags
NonSwag Apr 17, 2026
8f24ad7
Split metrics and flags url into separate values
NonSwag Apr 19, 2026
38ec98a
Throw on non-finite numbers
NonSwag Apr 19, 2026
d6298bc
Replace Object with JsonPrimitive in Attributes
NonSwag Apr 19, 2026
00d05b4
Add Type enum to FeatureFlags
NonSwag Apr 19, 2026
b60a3cf
Refactor SimpleFeatureFlagService
NonSwag Apr 19, 2026
1747156
Generalize terms in onboarding message and default config
NonSwag Apr 19, 2026
fcda864
Refactored logging
NonSwag Apr 19, 2026
7368f1d
Removed settings and ability to define metrics URL and debug
NonSwag Apr 19, 2026
71f31d3
Document FeatureFlags
NonSwag Apr 19, 2026
57a6612
Document Attributes#forEachPrimitive
NonSwag Apr 19, 2026
3c29b49
Use correct url
NonSwag Apr 19, 2026
d210cc2
Make SDK properties static
NonSwag Apr 19, 2026
32c4073
Add minimal logger api
NonSwag Apr 19, 2026
794be5f
Extracts constants to its own class
NonSwag Apr 19, 2026
81a9895
Add dedicated Hytale logger
NonSwag Apr 19, 2026
80910a7
Use custom filter predicate
NonSwag Apr 19, 2026
821e0c8
Move logger below metrics server url
NonSwag Apr 19, 2026
18acf03
Removed unused imports
NonSwag Apr 19, 2026
07e4a93
Replace Gson#toJson with toString
NonSwag Apr 19, 2026
92cb8d1
Add logger to feature flag service
NonSwag Apr 19, 2026
8d7b910
Decouple config from Metrics interface
NonSwag Apr 19, 2026
ceefb75
Undo happy little accident :)
NonSwag Apr 19, 2026
be86d49
Add info comments to example
NonSwag Apr 19, 2026
5f32c31
Throw on negative ttl
NonSwag Apr 19, 2026
8dcd796
Add attributes and TTL getters
NonSwag Apr 19, 2026
626d580
Refactor URL retrieval
NonSwag Apr 19, 2026
35aba15
Add `getLogger(Class)` overload
NonSwag Apr 19, 2026
b09e062
Decouple metrics and feature flags
NonSwag Apr 19, 2026
f0bbddb
Cancel all running fetches on shutdown
NonSwag Apr 19, 2026
a5614bf
Retrieve server id from config
NonSwag Apr 19, 2026
c9fe785
Unseal config
NonSwag Apr 19, 2026
5c2faa7
Update config comment
NonSwag Apr 19, 2026
b7794ee
Very elegant but sounds stupid
NonSwag Apr 19, 2026
379d039
Prepare for config impl extraction
NonSwag Apr 19, 2026
5c88ab8
todo
NonSwag Apr 19, 2026
c3a0bc1
Extract config impl to separate module
NonSwag Apr 19, 2026
6c8d5ac
Update plugin application code
NonSwag Apr 19, 2026
dbd791b
Refactor config handling
NonSwag Apr 20, 2026
5d834a3
Major metrics schema refactor
NonSwag Apr 20, 2026
8d6574c
Simplified metrics construction flow overhead
NonSwag Apr 21, 2026
f2f7679
Added injection support for platform context
NonSwag Apr 21, 2026
88799e0
Update examples to reflect the current best practices
NonSwag Apr 21, 2026
3b023a1
Pass the server id to feature flag service
NonSwag Apr 21, 2026
d02184f
Document SimpleContext constructor
NonSwag Apr 21, 2026
08abb27
Fix happy little accident
NonSwag Apr 21, 2026
061a838
Add more test coverage and fixed awful smoke tests
NonSwag Apr 21, 2026
58c2284
Add fabric client support
NonSwag Apr 21, 2026
f16e179
Stacktrace fingerprinting
NonSwag Apr 21, 2026
93b3e4e
Rename hash method to hash128 and replace JsonObject with String para…
NonSwag Apr 21, 2026
5aa8012
Rename isLibraryClass to isLibraryFrame
NonSwag Apr 21, 2026
15dbd82
Integrate stacktrace fingerprinting
NonSwag Apr 21, 2026
e0275aa
Add stacktrace fingerprinting tests
NonSwag Apr 21, 2026
2bfe20d
Fix inverted condition
NonSwag Apr 22, 2026
d76be21
Add method contracts
NonSwag Apr 22, 2026
994c0ae
Do not fetch eagerly
NonSwag Apr 22, 2026
ff13899
No need to cache the logger
NonSwag Apr 22, 2026
1170b48
Reword feature-flags virtual constructor javadocs description
NonSwag Apr 22, 2026
931b692
Move fetch times and cache to flag implementation
NonSwag Apr 22, 2026
7be1dc9
Add logger name to error log record
NonSwag Apr 22, 2026
6a44c81
Add debug logs to feature flag fetches
NonSwag Apr 22, 2026
4742b67
Note exceptional behavior for feature flag fetches
NonSwag Apr 22, 2026
e134130
Link to #fetch
NonSwag Apr 22, 2026
6232bfb
Link to #whenReady
NonSwag Apr 22, 2026
65638d0
Clarify #getChaged docs
NonSwag Apr 22, 2026
27551f5
Simplified code structure
NonSwag Apr 22, 2026
fca08ab
Update feature flags example
NonSwag Apr 22, 2026
92b4a84
Fix url resolving
NonSwag Apr 22, 2026
cb1e505
Rename serverId to identifier
NonSwag Apr 22, 2026
f7d21a3
Add more debug logs
NonSwag Apr 22, 2026
80158d8
Simplify opt request callback
NonSwag Apr 24, 2026
bb576bc
Simplify error tracker entry creation
NonSwag Apr 29, 2026
5d53885
Rename reported "hash" to "group_hash"
NonSwag Apr 29, 2026
9a82a55
remove useless test
NonSwag Apr 29, 2026
b7c359d
Remove opt-in and out API
NonSwag Apr 29, 2026
0105a47
Improve number and boolean parsing
NonSwag May 1, 2026
b110841
Remove error fingerprinting
NonSwag May 5, 2026
eef950d
Revert "Remove opt-in and out API"
NonSwag May 20, 2026
149d009
Add feature flag tests
NonSwag May 20, 2026
a6dd6a9
Add method contracts to implementations
NonSwag May 20, 2026
1035d9a
Remove #needsFlushing
NonSwag May 21, 2026
50746da
Rename `FastStatsContext#metrics` to `#metricsFactory`
NonSwag May 25, 2026
7a5e915
Refactor feature flags
NonSwag Apr 17, 2026
3bc9128
Add minimal logger api
NonSwag Apr 19, 2026
28c0296
Extract config impl to separate module
NonSwag Apr 19, 2026
5e9aa22
Major metrics schema refactor
NonSwag Apr 20, 2026
499a406
Revert "Remove error fingerprinting"
NonSwag May 5, 2026
9d03c8d
Remove redundant sponge config dependency
NonSwag May 21, 2026
2a18b92
Remove SettingsExample.java
NonSwag May 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 @@ -16,14 +16,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 @@ -93,7 +95,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 @@ -10,5 +10,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.metricsFactory()
// 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
41 changes: 41 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,41 @@
package dev.faststats.bukkit;

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

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
@Contract(value = " -> new", pure = true)
public BukkitMetrics.Factory metricsFactory() {
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;
}
}
65 changes: 32 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,16 @@
package dev.faststats.bukkit;

import com.google.gson.JsonObject;
import dev.faststats.core.SimpleMetrics;
import dev.faststats.ErrorTracker;
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 +22,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 +63,11 @@ private boolean isProxyOnlineMode() {
return settings.getBoolean("bungeecord") && proxies.getBoolean("bungee-cord.online-mode");
}

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

@Override
protected void appendDefaultData(final JsonObject metrics) {
metrics.addProperty("minecraft_version", minecraftVersion);
Expand All @@ -76,26 +81,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 +103,29 @@ 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 {
Factory(final BukkitContext context) {
super(context);
}

@Override
public Factory addMetric(final Metric<?> metric) throws IllegalArgumentException {
return (Factory) super.addMetric(metric);
}

@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 onFlush(final Runnable flush) {
return (Factory) super.onFlush(flush);
}

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 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
Loading