Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
be92623
Refactor feature flags
NonSwag Apr 17, 2026
1075b01
Split metrics and flags url into separate values
NonSwag Apr 19, 2026
8a50cb7
Throw on non-finite numbers
NonSwag Apr 19, 2026
24f52d6
Replace Object with JsonPrimitive in Attributes
NonSwag Apr 19, 2026
14c4fb5
Add Type enum to FeatureFlags
NonSwag Apr 19, 2026
eee9708
Refactor SimpleFeatureFlagService
NonSwag Apr 19, 2026
475e5c5
Generalize terms in onboarding message and default config
NonSwag Apr 19, 2026
b89b687
Refactored logging
NonSwag Apr 19, 2026
cff4cb4
Removed settings and ability to define metrics URL and debug
NonSwag Apr 19, 2026
b079fe5
Document FeatureFlags
NonSwag Apr 19, 2026
c166d31
Document Attributes#forEachPrimitive
NonSwag Apr 19, 2026
28e80f3
Use correct url
NonSwag Apr 19, 2026
3756832
Make SDK properties static
NonSwag Apr 19, 2026
e89921d
Add minimal logger api
NonSwag Apr 19, 2026
0ffb84e
Extracts constants to its own class
NonSwag Apr 19, 2026
e864680
Add dedicated Hytale logger
NonSwag Apr 19, 2026
7b9d10b
Use custom filter predicate
NonSwag Apr 19, 2026
75a6584
Move logger below metrics server url
NonSwag Apr 19, 2026
75e7056
Removed unused imports
NonSwag Apr 19, 2026
2952541
Replace Gson#toJson with toString
NonSwag Apr 19, 2026
70c1576
Add logger to feature flag service
NonSwag Apr 19, 2026
8a1a21a
Decouple config from Metrics interface
NonSwag Apr 19, 2026
2614ae4
Undo happy little accident :)
NonSwag Apr 19, 2026
742d6cf
Add info comments to example
NonSwag Apr 19, 2026
e38a890
Throw on negative ttl
NonSwag Apr 19, 2026
69fe8b0
Add attributes and TTL getters
NonSwag Apr 19, 2026
e63c991
Refactor URL retrieval
NonSwag Apr 19, 2026
fec2092
Add `getLogger(Class)` overload
NonSwag Apr 19, 2026
968fce9
Decouple metrics and feature flags
NonSwag Apr 19, 2026
c2cdd8d
Cancel all running fetches on shutdown
NonSwag Apr 19, 2026
d70b933
Retrieve server id from config
NonSwag Apr 19, 2026
39c2e7a
Unseal config
NonSwag Apr 19, 2026
3c9f63d
Update config comment
NonSwag Apr 19, 2026
ed5d3fc
Very elegant but sounds stupid
NonSwag Apr 19, 2026
423fc4b
Prepare for config impl extraction
NonSwag Apr 19, 2026
63fdf4b
todo
NonSwag Apr 19, 2026
0e38bee
Extract config impl to separate module
NonSwag Apr 19, 2026
ca525ef
Update plugin application code
NonSwag Apr 19, 2026
349a67a
Refactor config handling
NonSwag Apr 20, 2026
84b5029
Major metrics schema refactor
NonSwag Apr 20, 2026
45a5e66
Simplified metrics construction flow overhead
NonSwag Apr 21, 2026
411685f
Added injection support for platform context
NonSwag Apr 21, 2026
645fd72
Update examples to reflect the current best practices
NonSwag Apr 21, 2026
621039e
Pass the server id to feature flag service
NonSwag Apr 21, 2026
37e7fec
Document SimpleContext constructor
NonSwag Apr 21, 2026
454b026
Fix happy little accident
NonSwag Apr 21, 2026
7b74a1f
Add more test coverage and fixed awful smoke tests
NonSwag Apr 21, 2026
b852b64
Add fabric client support
NonSwag Apr 21, 2026
54e899b
Stacktrace fingerprinting
NonSwag Apr 21, 2026
bd4366e
Rename hash method to hash128 and replace JsonObject with String para…
NonSwag Apr 21, 2026
c1ecdd8
Rename isLibraryClass to isLibraryFrame
NonSwag Apr 21, 2026
fb7a5f8
Integrate stacktrace fingerprinting
NonSwag Apr 21, 2026
406e320
Add stacktrace fingerprinting tests
NonSwag Apr 21, 2026
2b09733
Fix inverted condition
NonSwag Apr 22, 2026
96d2039
Add method contracts
NonSwag Apr 22, 2026
4ae7f08
Do not fetch eagerly
NonSwag Apr 22, 2026
b4d5454
No need to cache the logger
NonSwag Apr 22, 2026
17ca59b
Reword feature-flags virtual constructor javadocs description
NonSwag Apr 22, 2026
02861c7
Move fetch times and cache to flag implementation
NonSwag Apr 22, 2026
f658c67
Add logger name to error log record
NonSwag Apr 22, 2026
f5fc862
Add debug logs to feature flag fetches
NonSwag Apr 22, 2026
5fb6779
Note exceptional behavior for feature flag fetches
NonSwag Apr 22, 2026
8b87431
Link to #fetch
NonSwag Apr 22, 2026
446887b
Link to #whenReady
NonSwag Apr 22, 2026
a30eb38
Clarify #getChaged docs
NonSwag Apr 22, 2026
8be66e7
Simplified code structure
NonSwag Apr 22, 2026
62e8b94
Update feature flags example
NonSwag Apr 22, 2026
f7862e2
Fix url resolving
NonSwag Apr 22, 2026
60a8c47
Rename serverId to identifier
NonSwag Apr 22, 2026
2986bbe
Add more debug logs
NonSwag Apr 22, 2026
8b42365
Simplify opt request callback
NonSwag Apr 24, 2026
b02a647
Simplify error tracker entry creation
NonSwag Apr 29, 2026
ca9f502
Rename reported "hash" to "group_hash"
NonSwag Apr 29, 2026
a226326
remove useless test
NonSwag Apr 29, 2026
f2429f1
Remove opt-in and out API
NonSwag Apr 29, 2026
12e7043
Improve number and boolean parsing
NonSwag May 1, 2026
f80aa84
Remove error fingerprinting
NonSwag May 5, 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;
}
}
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