From d2d0658a806ef97667f410e046078e0366172619 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sat, 31 Oct 2026 15:15:07 +0100 Subject: [PATCH 1/3] feat(metrics): report which features servers actually use Installed-only bStats data cannot show what is safe to remove. Count real invocations and which modules stay enabled, so unused features show up over time. --- .../modules/common/metrics/FeatureUsage.java | 37 +++++++++ .../common/metrics/MetricsManager.java | 83 ++++++++++++++++++- .../modules/common/metrics/UsageFeature.java | 33 ++++++++ .../generator/components/field/Field.java | 1 + .../generator/components/house/House.java | 1 + .../generator/components/kml/KmlCommand.java | 5 ++ .../generator/components/rail/Rail.java | 1 + .../generator/components/road/Road.java | 1 + .../generator/components/tree/Tree.java | 1 + .../generator/model/GeneratorComponent.java | 15 ++++ .../map/commands/MapCreateCommand.java | 3 + .../blockpalettegui/BlockPaletteCommand.java | 8 +- .../address/commands/AddressCommand.java | 6 +- .../navigator/commands/BuildteamCommand.java | 3 + .../navigator/commands/NavigatorCommand.java | 3 + .../listeners/NavigatorOpenListener.java | 4 + .../components/tpll/TpllComponent.java | 6 ++ .../components/warps/WarpsComponent.java | 7 ++ .../warps/commands/WarpCommand.java | 3 + .../modules/stats/StatsModule.java | 4 +- .../common/metrics/FeatureUsageTest.java | 56 +++++++++++++ 21 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java create mode 100644 src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java new file mode 100644 index 00000000..2a583393 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java @@ -0,0 +1,37 @@ +package net.buildtheearth.buildteamtools.modules.common.metrics; + +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Counts feature invocations since the last metrics submission. + * Call {@link #record(UsageFeature)} from the player action. {@link #drain()} is for the bStats callback. + */ +public final class FeatureUsage { + + private static final EnumMap COUNTS = new EnumMap<>(UsageFeature.class); + + static { + for (UsageFeature feature : UsageFeature.values()) + COUNTS.put(feature, new AtomicInteger()); + } + + private FeatureUsage() { + } + + public static void record(UsageFeature feature) { + COUNTS.get(feature).incrementAndGet(); + } + + /** + * Returns every feature, including those with zero uses, then resets the counters. + */ + static Map drain() { + Map snapshot = new LinkedHashMap<>(); + for (UsageFeature feature : UsageFeature.values()) + snapshot.put(feature.chartId(), COUNTS.get(feature).getAndSet(0)); + return snapshot; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java index 73ecee20..56cd5adb 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java @@ -1,13 +1,92 @@ package net.buildtheearth.buildteamtools.modules.common.metrics; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.ModuleHandler; +import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; +import net.buildtheearth.buildteamtools.modules.navigation.components.bluemap.BluemapComponent; import org.bstats.bukkit.Metrics; +import org.bstats.charts.AdvancedPie; +import org.bstats.charts.MultiLineChart; +import org.bstats.charts.SimpleBarChart; import org.bukkit.plugin.java.JavaPlugin; -public class MetricsManager { +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * bStats metrics for BuildTeamTools. + *

+ * {@code feature_uses} is a bar chart that lists every tracked feature. A zero means it was not used + * in that submission. bStats drops zero values from line charts, so {@code feature_use_history} repeats + * the same counts as a line chart and only draws features that were actually used. Both charts share + * one snapshot so a submission does not reset the counters twice. + *

+ * BlueMap has no player action in this plugin. It is reported as 1 while the integration is enabled. + */ +public final class MetricsManager { private static final int BSTATS_PLUGIN_ID = 29076; + /** Long enough for every chart in one submission to share a snapshot, shorter than the 30 minute submit interval. */ + private static final long SNAPSHOT_REUSE_NANOS = TimeUnit.MINUTES.toNanos(2); + + private static Metrics metrics; + private static Map publishedUses; + private static long publishedAtNanos = Long.MIN_VALUE; + + private MetricsManager() { + } public static void init(JavaPlugin plugin) { - new Metrics(plugin, BSTATS_PLUGIN_ID); + if (metrics != null) + metrics.shutdown(); + + Metrics created = new Metrics(plugin, BSTATS_PLUGIN_ID); + created.addCustomChart(new SimpleBarChart("feature_uses", MetricsManager::featureUseCounts)); + created.addCustomChart(new MultiLineChart("feature_use_history", MetricsManager::featureUseCounts)); + created.addCustomChart(new AdvancedPie("enabled_modules", MetricsManager::enabledModules)); + metrics = created; + } + + static Map featureUseCounts() { + return featureUseCounts(System.nanoTime()); + } + + static Map featureUseCounts(long nowNanos) { + synchronized (MetricsManager.class) { + if (publishedUses != null && nowNanos >= publishedAtNanos && nowNanos - publishedAtNanos < SNAPSHOT_REUSE_NANOS) + return new LinkedHashMap<>(publishedUses); + + Map counts = FeatureUsage.drain(); + counts.put("bluemap", blueMapInUse()); + publishedUses = counts; + publishedAtNanos = nowNanos; + return new LinkedHashMap<>(counts); + } + } + + static Map enabledModules() { + Map enabled = new LinkedHashMap<>(); + for (Module module : ModuleHandler.getInstance().getModules()) + if (module.isEnabled()) + enabled.put(module.getModuleName(), 1); + return enabled; + } + + private static int blueMapInUse() { + NavigationModule navigation = NavigationModule.getInstance(); + if (navigation == null) + return 0; + + BluemapComponent bluemap = navigation.getBluemapComponent(); + return bluemap != null && bluemap.isEnabled() ? 1 : 0; + } + + static void resetForTests() { + synchronized (MetricsManager.class) { + publishedUses = null; + publishedAtNanos = Long.MIN_VALUE; + } + FeatureUsage.drain(); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java new file mode 100644 index 00000000..27c454b4 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageFeature.java @@ -0,0 +1,33 @@ +package net.buildtheearth.buildteamtools.modules.common.metrics; + +/** + * Features whose invocations are reported to bStats. + *

+ * Plot system and the sign text generator have no live player entry point, so they are not listed here. + * Whether those modules stay enabled is covered by the enabled-modules chart. + */ +public enum UsageFeature { + HOUSE("house"), + ROAD("road"), + RAIL("rail"), + TREE("tree"), + FIELD("field"), + KML("kml"), + NAVIGATOR("navigator"), + WARPS("warps"), + TPLL("tpll"), + ADDRESS("address"), + MAP("map"), + STATS("stats"), + BLOCK_PALETTE("block_palette"); + + private final String chartId; + + UsageFeature(String chartId) { + this.chartId = chartId; + } + + public String chartId() { + return chartId; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java index 69df9d05..b652c007 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/field/Field.java @@ -25,6 +25,7 @@ public void generate(Player p) { if (!checkForPlayer(p)) return; + recordUse(); new FieldScripts(p, this); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java index c804522e..bcc0415a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/house/House.java @@ -33,6 +33,7 @@ public void generate(Player p) { if (!checkForPlayer(p)) return; + recordUse(); new HouseScripts(p, this); } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java index 26c16eb6..eb80a9cb 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java @@ -6,6 +6,8 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.utils.BlockLocation; import net.buildtheearth.buildteamtools.utils.LineRasterization; @@ -268,6 +270,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { return false; } + FeatureUsage.record(UsageFeature.KML); String kml_content = String.join(" ", args); Material matchedBlockMaterial = Material.matchMaterial(blocktypeString); @@ -498,6 +501,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { * @return true if the CommandBlock was created successfully */ public boolean createPasteUI(Player player, String alias, String @NonNull [] args) { + FeatureUsage.record(UsageFeature.KML); //The command either creates a command-block at the player location //arguments are an // optional blocktype @@ -580,6 +584,7 @@ public boolean undoCommand(Player player) { return false; } + FeatureUsage.record(UsageFeature.KML); ChangeTransaction transaction = playerHistory.pop(); transaction.undo(); player.sendMessage(String.format("undo successful. Restored %d blocks.", transaction.size())); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java index 02e11105..22f948dd 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java @@ -76,6 +76,7 @@ public void generate(Player player) { return; } + recordUse(); new RailScripts(player, this, () -> preparingPlayers.remove(player.getUniqueId())); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java index dc133d3f..894c35c1 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/road/Road.java @@ -22,6 +22,7 @@ public void generate(Player p) { if (!GeneratorModule.getInstance().getRoad().checkForPlayer(p)) return; + recordUse(); new RoadScripts(p, this); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java index 6c5e747f..72a954bf 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/tree/Tree.java @@ -52,6 +52,7 @@ public void generate(Player p) { if (!checkForPlayer(p)) return; + recordUse(); TreeScripts.treescript_v_1_0(p, this); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java index ff0d3639..00d459bf 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java @@ -5,6 +5,8 @@ import com.alpsbte.alpslib.utils.WikiDocumented; import lombok.Getter; import net.buildtheearth.buildteamtools.modules.ModuleComponent; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.generator.components.field.FieldSettings; import net.buildtheearth.buildteamtools.modules.generator.components.house.HouseSettings; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; @@ -39,6 +41,19 @@ protected GeneratorComponent(@NonNull GeneratorType type) { public abstract void generate(Player p); + /** + * Records a generation that got past the feature's own checks. + */ + protected void recordUse() { + FeatureUsage.record(switch (generatorType) { + case HOUSE -> UsageFeature.HOUSE; + case ROAD -> UsageFeature.ROAD; + case RAIL -> UsageFeature.RAIL; + case TREE -> UsageFeature.TREE; + case FIELD -> UsageFeature.FIELD; + }); + } + public void analyzeCommand(Player p, String[] args) { if (isHelpCommand(args)) { sendHelp(p); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java index 388cc364..a821a3d7 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/map/commands/MapCreateCommand.java @@ -21,6 +21,8 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.map.MapModule; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.utils.io.MapConfig; @@ -102,6 +104,7 @@ private void create(Player player, String claimName) { request.name(claimName); } + FeatureUsage.record(UsageFeature.MAP); player.sendMessage(ChatHelper.getStandardComponent(false, "Creating map claim from your selection...")); Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(), () -> submitClaim(player, request)); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java index 783a9be8..d1d9dc26 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java @@ -2,6 +2,8 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.kyori.adventure.text.Component; @@ -48,8 +50,10 @@ public void register(Module owner) { "Open the block palette and reset its filters.", builder -> builder.permission(Permissions.BLOCK_PALETTE_USE), context -> { CommandSender sender = context.sender().getSender(); - if (sender instanceof Player player) + if (sender instanceof Player player) { + FeatureUsage.record(UsageFeature.BLOCK_PALETTE); blockPalletManager.get().setPlayerFiltersAndOpen(player); + } else sender.sendMessage("This command can only be used by players."); }); @@ -76,6 +80,7 @@ public void register(Module owner) { Set newFilters = new HashSet<>(List.of(filterInput.split("\\s+"))); manager.updatePlayerFilters(player, newFilters); } + FeatureUsage.record(UsageFeature.BLOCK_PALETTE); new ChoosePaletteMenu(manager, player, plugin).open(); }); } @@ -90,6 +95,7 @@ private void execute(@NonNull CommandSender sender, String @NonNull [] args) { BlockPaletteManager manager = blockPalletManager.get(); // /bp ⇒ open block menu with remembered filters (do NOT reset to "color") if (args.length == 0) { + FeatureUsage.record(UsageFeature.BLOCK_PALETTE); manager.openBlockMenu(player); return; } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java index c2861c3d..aedee053 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java @@ -5,8 +5,10 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; -import net.buildtheearth.buildteamtools.modules.network.api.NominatimAPI; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.network.api.PhotonAPI; +import net.buildtheearth.buildteamtools.modules.network.api.NominatimAPI; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.model.GeographicalCoordinate; import net.kyori.adventure.text.Component; @@ -86,6 +88,7 @@ private void handleGetCommand(@NonNull Player player, boolean international) { return; } + FeatureUsage.record(UsageDetail.ADDRESS_GET); player.sendMessage("Getting closest address..."); final GeographicalCoordinate geoCoord; @@ -173,6 +176,7 @@ private void handleTeleportCommand(@NonNull Player player, String address) { return; } + FeatureUsage.record(UsageDetail.ADDRESS_TELEPORT); String lang = player.locale().getLanguage(); PhotonAPI.getCoordinatesFromAddressAsync(address, lang) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java index 6a5f36ed..d342a6f9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java @@ -4,6 +4,8 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; @@ -51,6 +53,7 @@ protected void btCommand(@NonNull CommandSender sender, String buildTeamName, St } public void execute(Player player, BuildTeam team) { + FeatureUsage.record(UsageFeature.NAVIGATOR); NavUtils.switchToTeam(team, player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java index 9d9517ad..461abbe5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java @@ -5,6 +5,8 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; import net.buildtheearth.buildteamtools.modules.navigation.menu.CitySelectorMenu; import net.buildtheearth.buildteamtools.modules.navigation.menu.CountrySelectorMenu; @@ -134,6 +136,7 @@ private void openPlayer(@NonNull CommandSender sender, Consumer action) sendPlayerOnlyMessage(sender); return; } + FeatureUsage.record(UsageFeature.NAVIGATOR); action.accept(player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java index f895305f..b465e46e 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java @@ -1,5 +1,7 @@ package net.buildtheearth.buildteamtools.modules.navigation.components.navigator.listeners; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; import net.buildtheearth.buildteamtools.modules.navigation.menu.MainMenu; import org.bukkit.entity.Player; @@ -22,6 +24,7 @@ public void interactEvent(@NonNull PlayerInteractEvent event) { if (NavigationModule.getInstance().getNavigatorComponent().isItem(event.getItem())) { event.setCancelled(true); + FeatureUsage.record(UsageFeature.NAVIGATOR); new MainMenu(event.getPlayer()); } } @@ -45,6 +48,7 @@ public void onClick(@NonNull InventoryClickEvent event) { } // Opens the navigator. + FeatureUsage.record(UsageFeature.NAVIGATOR); new MainMenu(player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java index e41acc40..529de31b 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/tpll/TpllComponent.java @@ -6,6 +6,8 @@ import com.google.common.io.ByteStreams; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.ModuleComponent; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; @@ -79,6 +81,7 @@ public void processQueueForPlayer(@NonNull Player player) { return; } + FeatureUsage.record(UsageFeature.TPLL); player.teleport(tpllTarget); ChatHelper.logDebug("Teleported the player to the tpll target."); tpllQueue.remove(player.getUniqueId()); @@ -93,6 +96,7 @@ public void processQueueForPlayer(@NonNull Player player) { * @param targetServerName The server to send the player to. */ public void tpllPlayer(@NotNull Player player, @NotNull GeographicalCoordinate coordinate, String targetServerName) { + FeatureUsage.record(UsageFeature.TPLL); ChatHelper.logDebug("Starting universal tpll teleportation for %s to %s.", player.getDisplayName(), targetServerName); // Send a plugin message to the target server which adds the tpll to the queue ByteArrayDataOutput out = ByteStreams.newDataOutput(); @@ -108,6 +112,7 @@ public void tpllPlayer(@NotNull Player player, @NotNull GeographicalCoordinate c } public void tpllPlayerTransfer(@NotNull Player player, @NotNull GeographicalCoordinate coordinate, String ip) { + FeatureUsage.record(UsageFeature.TPLL); ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeDouble(coordinate.latitude()); out.writeDouble(coordinate.longitude()); @@ -120,6 +125,7 @@ public void processCookie(@NotNull Player player, byte[] cookie) { double targetLatitude = in.readDouble(); double targetLongitude = in.readDouble(); ChatHelper.logDebug("Processing cookie for tpll event: lat: %s lon: %s", targetLatitude, targetLongitude); + FeatureUsage.record(UsageFeature.TPLL); Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), // Needs to be delayed if not exception will be trown and nothing happens () -> player.performCommand("tpll " + targetLatitude + " " + targetLongitude)); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java index 98dedcc9..1eb9271a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java @@ -8,6 +8,8 @@ import net.buildtheearth.Projection; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.ModuleComponent; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpEditMenu; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpGroupEditMenu; @@ -118,6 +120,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { loc.setY(warp.getY()); + FeatureUsage.record(UsageFeature.WARPS); player.teleportAsync(loc); ChatHelper.sendSuccessfulMessage(player, "Successfully warped you to %s.", warp.getName()); return; @@ -128,6 +131,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { var type = NavUtils.determineSwitchPossibilityOrMsgPlayerIfNone(player, warp.getWarpGroup().getBuildTeam()); if (type == NavUtils.NavSwitchType.NETWORK) { + FeatureUsage.record(UsageFeature.WARPS); // Send a plugin message to that server which adds the warp to the queue ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("UniversalWarps"); @@ -138,6 +142,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { // Switch the player to the target server NetworkModule.getInstance().switchServer(player, warp.getWarpGroup().getBuildTeam().getServerName()); } else if (type == NavUtils.NavSwitchType.TRANSFER) { + FeatureUsage.record(UsageFeature.WARPS); ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF(String.valueOf(warp.getId())); player.storeCookie(WARP_COOKIE_KEY, out.toByteArray()); @@ -166,6 +171,7 @@ public static void createWarp(Player creator) { * @param creator The player that is creating the warp */ public static void createWarp(@NonNull Player creator, WarpGroup group) { + FeatureUsage.record(UsageFeature.WARPS); // Get the geographic coordinates of the player's location. Location location = creator.getLocation(); try { @@ -270,6 +276,7 @@ public static void openWarpMenu(@NotNull Player player) { public static void openWarpMenu(@NotNull Player player, @NotNull BuildTeam buildTeam, @Nullable AbstractMenu menu) { if (buildTeam.getWarpGroups() == null) return; + FeatureUsage.record(UsageFeature.WARPS); int warpGroupCount = buildTeam.getWarpGroups().size(); switch (warpGroupCount) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java index 709dc0dd..458c5105 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java @@ -11,6 +11,8 @@ import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.utils.Utils; import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; @@ -106,6 +108,7 @@ private void handleMigrateCommand(@NonNull Player player, WarpMigrationSource so return; } + FeatureUsage.record(UsageFeature.WARPS); WarpMigrator migrator = new WarpMigrator(source); player.sendMessage(ChatHelper.getStandardComponent(true, "Migrating the warps...")); migrator.migrate(player).whenComplete((result, throwable) -> diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java index 6b9a79d1..5112ee5d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/stats/StatsModule.java @@ -5,6 +5,8 @@ import lombok.Getter; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.stats.listeners.StatsListener; import net.buildtheearth.buildteamtools.modules.stats.model.StatsPlayer; @@ -118,7 +120,7 @@ public boolean updateAndSave() { out.writeUTF(StatsModule.getInstance().getCurrentCache().toJSONString()); p.sendPluginMessage(BuildTeamTools.getInstance(), "btt:buildteam", out.toByteArray()); - + FeatureUsage.record(UsageFeature.STATS); return true; } diff --git a/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java new file mode 100644 index 00000000..c22be54a --- /dev/null +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java @@ -0,0 +1,56 @@ +package net.buildtheearth.buildteamtools.modules.common.metrics; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FeatureUsageTest { + + @BeforeEach + void resetMetrics() { + MetricsManager.resetForTests(); + } + + @Test + void drainReturnsEveryFeatureAndResetsTheCounters() { + FeatureUsage.record(UsageFeature.HOUSE); + FeatureUsage.record(UsageFeature.HOUSE); + FeatureUsage.record(UsageFeature.KML); + + Map first = FeatureUsage.drain(); + assertEquals(UsageFeature.values().length, first.size()); + assertEquals(2, first.get("house")); + assertEquals(1, first.get("kml")); + assertEquals(0, first.get("field")); + assertEquals(0, first.get("block_palette")); + + Map second = FeatureUsage.drain(); + assertEquals(0, second.get("house")); + assertEquals(0, second.get("kml")); + } + + @Test + void bothChartsShareOneSnapshotUntilTheNextSubmission() { + FeatureUsage.record(UsageFeature.ROAD); + + Map first = MetricsManager.featureUseCounts(0); + assertEquals(1, first.get("road")); + assertEquals(0, first.get("house")); + assertEquals(0, first.get("bluemap")); + assertEquals(UsageFeature.values().length + 1, first.size()); + + FeatureUsage.record(UsageFeature.TREE); + Map reused = MetricsManager.featureUseCounts(TimeUnit.SECONDS.toNanos(30)); + assertEquals(1, reused.get("road")); + assertEquals(0, reused.get("tree")); + + Map nextSubmission = MetricsManager.featureUseCounts(TimeUnit.MINUTES.toNanos(2)); + assertEquals(0, nextSubmission.get("road")); + assertEquals(1, nextSubmission.get("tree")); + assertEquals(0, nextSubmission.get("bluemap")); + } +} From 311f8d45d8243141fe592b2731a0e2d6d7f62fdd Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sat, 31 Oct 2026 16:00:41 +0100 Subject: [PATCH 2/3] feat(metrics): add drilldown breakdown for navigator, warps, address and kml Adds a bStats drilldown pie so a click on a feature shows which of its commands or menus were used. Each part also counts toward its parent feature, so the summary charts are unchanged. --- .../modules/common/metrics/FeatureUsage.java | 27 ++++++++++- .../common/metrics/MetricsManager.java | 42 ++++++++++++---- .../modules/common/metrics/UsageDetail.java | 48 +++++++++++++++++++ .../generator/components/kml/KmlCommand.java | 21 ++++++-- .../navigator/commands/BuildteamCommand.java | 4 +- .../navigator/commands/NavigatorCommand.java | 29 ++++++----- .../listeners/NavigatorOpenListener.java | 6 +-- .../components/warps/WarpsComponent.java | 12 ++--- .../warps/commands/WarpCommand.java | 4 +- .../common/metrics/FeatureUsageTest.java | 33 +++++++++++++ 10 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java index 2a583393..fa82dc50 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java @@ -7,15 +7,21 @@ /** * Counts feature invocations since the last metrics submission. - * Call {@link #record(UsageFeature)} from the player action. {@link #drain()} is for the bStats callback. + *

+ * {@link #record(UsageFeature)} counts a feature with no further breakdown. + * {@link #record(UsageDetail)} counts that part and its parent feature, so the summary charts + * stay aligned with the drilldown pie. */ public final class FeatureUsage { private static final EnumMap COUNTS = new EnumMap<>(UsageFeature.class); + private static final EnumMap DETAILS = new EnumMap<>(UsageDetail.class); static { for (UsageFeature feature : UsageFeature.values()) COUNTS.put(feature, new AtomicInteger()); + for (UsageDetail detail : UsageDetail.values()) + DETAILS.put(detail, new AtomicInteger()); } private FeatureUsage() { @@ -25,8 +31,13 @@ public static void record(UsageFeature feature) { COUNTS.get(feature).incrementAndGet(); } + public static void record(UsageDetail detail) { + record(detail.feature()); + DETAILS.get(detail).incrementAndGet(); + } + /** - * Returns every feature, including those with zero uses, then resets the counters. + * Returns every feature, including those with zero uses, then resets the feature counters. */ static Map drain() { Map snapshot = new LinkedHashMap<>(); @@ -34,4 +45,16 @@ static Map drain() { snapshot.put(feature.chartId(), COUNTS.get(feature).getAndSet(0)); return snapshot; } + + /** + * Returns every part of the features that have a breakdown, including zeros, then resets those counters. + */ + static Map> drainDetails() { + Map> snapshot = new LinkedHashMap<>(); + for (UsageDetail detail : UsageDetail.values()) { + snapshot.computeIfAbsent(detail.feature().chartId(), ignored -> new LinkedHashMap<>()) + .put(detail.chartId(), DETAILS.get(detail).getAndSet(0)); + } + return snapshot; + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java index 56cd5adb..a6cc3d9f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java @@ -6,6 +6,7 @@ import net.buildtheearth.buildteamtools.modules.navigation.components.bluemap.BluemapComponent; import org.bstats.bukkit.Metrics; import org.bstats.charts.AdvancedPie; +import org.bstats.charts.DrilldownPie; import org.bstats.charts.MultiLineChart; import org.bstats.charts.SimpleBarChart; import org.bukkit.plugin.java.JavaPlugin; @@ -23,6 +24,9 @@ * one snapshot so a submission does not reset the counters twice. *

* BlueMap has no player action in this plugin. It is reported as 1 while the integration is enabled. + *

+ * {@code feature_details} is a drilldown pie. Clicking navigator, warps, address, or kml opens the + * part that was used. It shares the snapshot with the summary charts. */ public final class MetricsManager { @@ -31,8 +35,7 @@ public final class MetricsManager { private static final long SNAPSHOT_REUSE_NANOS = TimeUnit.MINUTES.toNanos(2); private static Metrics metrics; - private static Map publishedUses; - private static long publishedAtNanos = Long.MIN_VALUE; + private static PublishedSnapshot published; private MetricsManager() { } @@ -44,6 +47,7 @@ public static void init(JavaPlugin plugin) { Metrics created = new Metrics(plugin, BSTATS_PLUGIN_ID); created.addCustomChart(new SimpleBarChart("feature_uses", MetricsManager::featureUseCounts)); created.addCustomChart(new MultiLineChart("feature_use_history", MetricsManager::featureUseCounts)); + created.addCustomChart(new DrilldownPie("feature_details", MetricsManager::featureDetails)); created.addCustomChart(new AdvancedPie("enabled_modules", MetricsManager::enabledModules)); metrics = created; } @@ -53,15 +57,28 @@ static Map featureUseCounts() { } static Map featureUseCounts(long nowNanos) { + return new LinkedHashMap<>(publish(nowNanos).counts()); + } + + static Map> featureDetails() { + return featureDetails(System.nanoTime()); + } + + static Map> featureDetails(long nowNanos) { + Map> copy = new LinkedHashMap<>(); + publish(nowNanos).details().forEach((feature, parts) -> copy.put(feature, new LinkedHashMap<>(parts))); + return copy; + } + + private static PublishedSnapshot publish(long nowNanos) { synchronized (MetricsManager.class) { - if (publishedUses != null && nowNanos >= publishedAtNanos && nowNanos - publishedAtNanos < SNAPSHOT_REUSE_NANOS) - return new LinkedHashMap<>(publishedUses); + if (published != null && nowNanos >= published.atNanos() && nowNanos - published.atNanos() < SNAPSHOT_REUSE_NANOS) + return published; Map counts = FeatureUsage.drain(); counts.put("bluemap", blueMapInUse()); - publishedUses = counts; - publishedAtNanos = nowNanos; - return new LinkedHashMap<>(counts); + published = new PublishedSnapshot(counts, FeatureUsage.drainDetails(), nowNanos); + return published; } } @@ -84,9 +101,16 @@ private static int blueMapInUse() { static void resetForTests() { synchronized (MetricsManager.class) { - publishedUses = null; - publishedAtNanos = Long.MIN_VALUE; + published = null; } FeatureUsage.drain(); + FeatureUsage.drainDetails(); + } + + private record PublishedSnapshot( + Map counts, + Map> details, + long atNanos + ) { } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java new file mode 100644 index 00000000..06b7baa3 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/UsageDetail.java @@ -0,0 +1,48 @@ +package net.buildtheearth.buildteamtools.modules.common.metrics; + +/** + * A part of a {@link UsageFeature} shown when that feature is opened on the bStats drilldown pie. + */ +public enum UsageDetail { + NAVIGATOR_MENU(UsageFeature.NAVIGATOR, "menu"), + NAVIGATOR_ITEM(UsageFeature.NAVIGATOR, "item"), + NAVIGATOR_EXPLORE(UsageFeature.NAVIGATOR, "explore"), + NAVIGATOR_COUNTRIES(UsageFeature.NAVIGATOR, "countries"), + NAVIGATOR_STATES(UsageFeature.NAVIGATOR, "states"), + NAVIGATOR_CITIES(UsageFeature.NAVIGATOR, "cities"), + NAVIGATOR_BUILD(UsageFeature.NAVIGATOR, "build"), + NAVIGATOR_PLOTSYSTEM(UsageFeature.NAVIGATOR, "plotsystem"), + NAVIGATOR_TUTORIALS(UsageFeature.NAVIGATOR, "tutorials"), + NAVIGATOR_TOGGLE(UsageFeature.NAVIGATOR, "toggle"), + NAVIGATOR_BUILDTEAM(UsageFeature.NAVIGATOR, "buildteam"), + + WARPS_MENU(UsageFeature.WARPS, "menu"), + WARPS_TELEPORT(UsageFeature.WARPS, "teleport"), + WARPS_CREATE(UsageFeature.WARPS, "create"), + WARPS_MIGRATE(UsageFeature.WARPS, "migrate"), + + ADDRESS_GET(UsageFeature.ADDRESS, "get"), + ADDRESS_TELEPORT(UsageFeature.ADDRESS, "teleport"), + + KML_GEOPOINTS(UsageFeature.KML, "geopoints"), + KML_GEOPATH(UsageFeature.KML, "geopath"), + KML_GEORING(UsageFeature.KML, "georing"), + KML_GEOSURFACE(UsageFeature.KML, "geosurface"), + KML_UNDO(UsageFeature.KML, "undo"); + + private final UsageFeature feature; + private final String chartId; + + UsageDetail(UsageFeature feature, String chartId) { + this.feature = feature; + this.chartId = chartId; + } + + public UsageFeature feature() { + return feature; + } + + public String chartId() { + return chartId; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java index eb80a9cb..10d6c2d6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java @@ -7,6 +7,7 @@ import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.utils.BlockLocation; @@ -270,7 +271,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { return false; } - FeatureUsage.record(UsageFeature.KML); + recordKml(blockCreationCommand); String kml_content = String.join(" ", args); Material matchedBlockMaterial = Material.matchMaterial(blocktypeString); @@ -501,7 +502,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { * @return true if the CommandBlock was created successfully */ public boolean createPasteUI(Player player, String alias, String @NonNull [] args) { - FeatureUsage.record(UsageFeature.KML); + recordKml(alias); //The command either creates a command-block at the player location //arguments are an // optional blocktype @@ -584,7 +585,7 @@ public boolean undoCommand(Player player) { return false; } - FeatureUsage.record(UsageFeature.KML); + FeatureUsage.record(UsageDetail.KML_UNDO); ChangeTransaction transaction = playerHistory.pop(); transaction.undo(); player.sendMessage(String.format("undo successful. Restored %d blocks.", transaction.size())); @@ -678,6 +679,20 @@ public boolean undoCommand(Player player) { return mcLines; } + private static void recordKml(@NonNull String command) { + UsageDetail detail = switch (command) { + case GEOPOINTS_COMMAND -> UsageDetail.KML_GEOPOINTS; + case GEOPATH_COMMAND -> UsageDetail.KML_GEOPATH; + case GEORING_COMMAND -> UsageDetail.KML_GEORING; + case GEOSURFACE_COMMAND -> UsageDetail.KML_GEOSURFACE; + default -> null; + }; + if (detail == null) + FeatureUsage.record(UsageFeature.KML); + else + FeatureUsage.record(detail); + } + /** * Maps command aliases to their corresponding block creation modes. *

diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java index d342a6f9..48a8b610 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java @@ -5,7 +5,7 @@ import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; -import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; @@ -53,7 +53,7 @@ protected void btCommand(@NonNull CommandSender sender, String buildTeamName, St } public void execute(Player player, BuildTeam team) { - FeatureUsage.record(UsageFeature.NAVIGATOR); + FeatureUsage.record(UsageDetail.NAVIGATOR_BUILDTEAM); NavUtils.switchToTeam(team, player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java index 461abbe5..2f468911 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java @@ -6,7 +6,7 @@ import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; -import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; import net.buildtheearth.buildteamtools.modules.navigation.menu.CitySelectorMenu; import net.buildtheearth.buildteamtools.modules.navigation.menu.CountrySelectorMenu; @@ -42,7 +42,8 @@ public void register(Module owner) { var commandManager = BuildTeamTools.getInstance().getCommandManager(); commandManager.registerNoArguments(owner, "navigator", Permissions.NAVIGATOR_USE, this::openNavigator); registerSubcommand(owner, "explore", "Open the continent selection menu.", - context -> openPlayer(context.sender().getSender(), player -> new ExploreMenu(player, true))); + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_EXPLORE, + player -> new ExploreMenu(player, true))); registerSubcommand(owner, "countries", "Open the country selection menu for a continent.", builder -> builder .permission(Permissions.NAVIGATOR_USE) @@ -58,10 +59,11 @@ public void register(Module owner) { String.join(", ", continentNames()))); return; } - openPlayer(sender, player -> new CountrySelectorMenu(player, continent, true)); + openPlayer(sender, UsageDetail.NAVIGATOR_COUNTRIES, + player -> new CountrySelectorMenu(player, continent, true)); }); registerSubcommand(owner, "states", "Open the USA state selection menu.", - context -> openPlayer(context.sender().getSender(), + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_STATES, player -> new StateSelectorMenu(player, Continent.NORTH_AMERICA, true))); registerSubcommand(owner, "cities", "Open the city selection menu for a supported state.", builder -> builder @@ -82,25 +84,28 @@ public void register(Module owner) { String.join(", ", cityStateNames()))); return; } - openPlayer(sender, player -> new CitySelectorMenu( + openPlayer(sender, UsageDetail.NAVIGATOR_CITIES, player -> new CitySelectorMenu( state, StateSelectorMenu.getCityTeamsForState(state), player, true)); }); // The module constructor registers commands before its own config field is set NavigationConfig.MainMenuItems items = BuildTeamTools.getInstance().getNavigationConfig().mainMenuItems(); if (items.buildItem().enabled()) { registerSubcommand(owner, "build", "Run the configured Build menu action.", - context -> openPlayer(context.sender().getSender(), MainMenu::openBuild)); + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_BUILD, + MainMenu::openBuild)); } if (items.plotsystemItem().enabled()) { registerSubcommand(owner, "plotsystem", "Run the configured Plot System menu action.", - context -> openPlayer(context.sender().getSender(), MainMenu::openPlotSystem)); + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_PLOTSYSTEM, + MainMenu::openPlotSystem)); } if (items.tutorialItem().enabled()) { registerSubcommand(owner, "tutorials", "Open the tutorial menu or run its configured action.", - context -> openPlayer(context.sender().getSender(), MainMenu::openTutorials)); + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_TUTORIALS, + MainMenu::openTutorials)); } registerSubcommand(owner, "toggle", "Toggle the navigator hotbar item.", - context -> openPlayer(context.sender().getSender(), + context -> openPlayer(context.sender().getSender(), UsageDetail.NAVIGATOR_TOGGLE, NavigationModule.getInstance().getNavigatorComponent()::toggle)); } @@ -128,15 +133,15 @@ private void registerSubcommand( } private void openNavigator(@NonNull CommandSender sender) { - openPlayer(sender, MainMenu::new); + openPlayer(sender, UsageDetail.NAVIGATOR_MENU, MainMenu::new); } - private void openPlayer(@NonNull CommandSender sender, Consumer action) { + private void openPlayer(@NonNull CommandSender sender, UsageDetail detail, Consumer action) { if (!(sender instanceof Player player)) { sendPlayerOnlyMessage(sender); return; } - FeatureUsage.record(UsageFeature.NAVIGATOR); + FeatureUsage.record(detail); action.accept(player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java index b465e46e..0484b09b 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/listeners/NavigatorOpenListener.java @@ -1,7 +1,7 @@ package net.buildtheearth.buildteamtools.modules.navigation.components.navigator.listeners; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; -import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; import net.buildtheearth.buildteamtools.modules.navigation.menu.MainMenu; import org.bukkit.entity.Player; @@ -24,7 +24,7 @@ public void interactEvent(@NonNull PlayerInteractEvent event) { if (NavigationModule.getInstance().getNavigatorComponent().isItem(event.getItem())) { event.setCancelled(true); - FeatureUsage.record(UsageFeature.NAVIGATOR); + FeatureUsage.record(UsageDetail.NAVIGATOR_ITEM); new MainMenu(event.getPlayer()); } } @@ -48,7 +48,7 @@ public void onClick(@NonNull InventoryClickEvent event) { } // Opens the navigator. - FeatureUsage.record(UsageFeature.NAVIGATOR); + FeatureUsage.record(UsageDetail.NAVIGATOR_ITEM); new MainMenu(player); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java index 1eb9271a..17d2647f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java @@ -9,7 +9,7 @@ import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.ModuleComponent; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; -import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpEditMenu; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.menu.WarpGroupEditMenu; @@ -120,7 +120,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { loc.setY(warp.getY()); - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_TELEPORT); player.teleportAsync(loc); ChatHelper.sendSuccessfulMessage(player, "Successfully warped you to %s.", warp.getName()); return; @@ -131,7 +131,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { var type = NavUtils.determineSwitchPossibilityOrMsgPlayerIfNone(player, warp.getWarpGroup().getBuildTeam()); if (type == NavUtils.NavSwitchType.NETWORK) { - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_TELEPORT); // Send a plugin message to that server which adds the warp to the queue ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("UniversalWarps"); @@ -142,7 +142,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { // Switch the player to the target server NetworkModule.getInstance().switchServer(player, warp.getWarpGroup().getBuildTeam().getServerName()); } else if (type == NavUtils.NavSwitchType.TRANSFER) { - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_TELEPORT); ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF(String.valueOf(warp.getId())); player.storeCookie(WARP_COOKIE_KEY, out.toByteArray()); @@ -171,7 +171,7 @@ public static void createWarp(Player creator) { * @param creator The player that is creating the warp */ public static void createWarp(@NonNull Player creator, WarpGroup group) { - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_CREATE); // Get the geographic coordinates of the player's location. Location location = creator.getLocation(); try { @@ -276,7 +276,7 @@ public static void openWarpMenu(@NotNull Player player) { public static void openWarpMenu(@NotNull Player player, @NotNull BuildTeam buildTeam, @Nullable AbstractMenu menu) { if (buildTeam.getWarpGroups() == null) return; - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_MENU); int warpGroupCount = buildTeam.getWarpGroups().size(); switch (warpGroupCount) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java index 458c5105..732b36c0 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java @@ -12,7 +12,7 @@ import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.common.metrics.FeatureUsage; -import net.buildtheearth.buildteamtools.modules.common.metrics.UsageFeature; +import net.buildtheearth.buildteamtools.modules.common.metrics.UsageDetail; import net.buildtheearth.buildteamtools.utils.Utils; import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; @@ -108,7 +108,7 @@ private void handleMigrateCommand(@NonNull Player player, WarpMigrationSource so return; } - FeatureUsage.record(UsageFeature.WARPS); + FeatureUsage.record(UsageDetail.WARPS_MIGRATE); WarpMigrator migrator = new WarpMigrator(source); player.sendMessage(ChatHelper.getStandardComponent(true, "Migrating the warps...")); migrator.migrate(player).whenComplete((result, throwable) -> diff --git a/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java index c22be54a..489b119c 100644 --- a/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java @@ -53,4 +53,37 @@ void bothChartsShareOneSnapshotUntilTheNextSubmission() { assertEquals(1, nextSubmission.get("tree")); assertEquals(0, nextSubmission.get("bluemap")); } + + @Test + void drilldownCountsThePartAndItsParent() { + FeatureUsage.record(UsageDetail.NAVIGATOR_EXPLORE); + FeatureUsage.record(UsageDetail.NAVIGATOR_EXPLORE); + FeatureUsage.record(UsageDetail.ADDRESS_GET); + FeatureUsage.record(UsageDetail.KML_UNDO); + + Map counts = MetricsManager.featureUseCounts(0); + assertEquals(2, counts.get("navigator")); + assertEquals(1, counts.get("address")); + assertEquals(1, counts.get("kml")); + assertEquals(0, counts.get("warps")); + + Map> details = MetricsManager.featureDetails(0); + assertEquals(2, details.get("navigator").get("explore")); + assertEquals(0, details.get("navigator").get("cities")); + assertEquals(0, details.get("navigator").get("buildteam")); + assertEquals(1, details.get("address").get("get")); + assertEquals(0, details.get("address").get("teleport")); + assertEquals(1, details.get("kml").get("undo")); + assertEquals(0, details.get("kml").get("geopoints")); + assertEquals(0, details.get("warps").get("migrate")); + assertEquals(4, details.size()); + + FeatureUsage.record(UsageDetail.WARPS_MENU); + Map> reused = MetricsManager.featureDetails(TimeUnit.SECONDS.toNanos(30)); + assertEquals(0, reused.get("warps").get("menu")); + + Map> nextSubmission = MetricsManager.featureDetails(TimeUnit.MINUTES.toNanos(2)); + assertEquals(1, nextSubmission.get("warps").get("menu")); + assertEquals(0, nextSubmission.get("navigator").get("explore")); + } } From f24b8a524bfa0136b93c67ed0ae321a03329e0d2 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sat, 31 Oct 2026 16:16:50 +0100 Subject: [PATCH 3/3] fix(metrics): drop feature_use_history until bStats supports multi line charts bStats does not offer multi line charts yet, so the chart could not be created on the website. feature_uses already lists every feature with zeros. --- .../modules/common/metrics/MetricsManager.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java index a6cc3d9f..f8ec70e0 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/MetricsManager.java @@ -7,7 +7,6 @@ import org.bstats.bukkit.Metrics; import org.bstats.charts.AdvancedPie; import org.bstats.charts.DrilldownPie; -import org.bstats.charts.MultiLineChart; import org.bstats.charts.SimpleBarChart; import org.bukkit.plugin.java.JavaPlugin; @@ -19,9 +18,8 @@ * bStats metrics for BuildTeamTools. *

* {@code feature_uses} is a bar chart that lists every tracked feature. A zero means it was not used - * in that submission. bStats drops zero values from line charts, so {@code feature_use_history} repeats - * the same counts as a line chart and only draws features that were actually used. Both charts share - * one snapshot so a submission does not reset the counters twice. + * in that submission. It shares one snapshot with {@code feature_details} so a submission does not + * reset the counters twice. *

* BlueMap has no player action in this plugin. It is reported as 1 while the integration is enabled. *

@@ -46,7 +44,6 @@ public static void init(JavaPlugin plugin) { Metrics created = new Metrics(plugin, BSTATS_PLUGIN_ID); created.addCustomChart(new SimpleBarChart("feature_uses", MetricsManager::featureUseCounts)); - created.addCustomChart(new MultiLineChart("feature_use_history", MetricsManager::featureUseCounts)); created.addCustomChart(new DrilldownPie("feature_details", MetricsManager::featureDetails)); created.addCustomChart(new AdvancedPie("enabled_modules", MetricsManager::enabledModules)); metrics = created;