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..fa82dc50 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsage.java @@ -0,0 +1,60 @@ +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. + *

+ * {@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() { + } + + 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 feature counters. + */ + static Map drain() { + Map snapshot = new LinkedHashMap<>(); + for (UsageFeature feature : UsageFeature.values()) + 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 73ecee20..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 @@ -1,13 +1,113 @@ 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.DrilldownPie; +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. 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. + *

+ * {@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 { 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 PublishedSnapshot published; + + 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 DrilldownPie("feature_details", MetricsManager::featureDetails)); + created.addCustomChart(new AdvancedPie("enabled_modules", MetricsManager::enabledModules)); + metrics = created; + } + + static Map featureUseCounts() { + return featureUseCounts(System.nanoTime()); + } + + 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 (published != null && nowNanos >= published.atNanos() && nowNanos - published.atNanos() < SNAPSHOT_REUSE_NANOS) + return published; + + Map counts = FeatureUsage.drain(); + counts.put("bluemap", blueMapInUse()); + published = new PublishedSnapshot(counts, FeatureUsage.drainDetails(), nowNanos); + return published; + } + } + + 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) { + 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/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..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 @@ -6,6 +6,9 @@ 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.UsageDetail; +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 +271,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { return false; } + recordKml(blockCreationCommand); String kml_content = String.join(" ", args); Material matchedBlockMaterial = Material.matchMaterial(blocktypeString); @@ -498,6 +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) { + recordKml(alias); //The command either creates a command-block at the player location //arguments are an // optional blocktype @@ -580,6 +585,7 @@ public boolean undoCommand(Player player) { return false; } + FeatureUsage.record(UsageDetail.KML_UNDO); ChangeTransaction transaction = playerHistory.pop(); transaction.undo(); player.sendMessage(String.format("undo successful. Restored %d blocks.", transaction.size())); @@ -673,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/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..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 @@ -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.UsageDetail; 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(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 9d9517ad..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 @@ -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.UsageDetail; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; import net.buildtheearth.buildteamtools.modules.navigation.menu.CitySelectorMenu; import net.buildtheearth.buildteamtools.modules.navigation.menu.CountrySelectorMenu; @@ -40,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) @@ -56,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 @@ -80,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)); } @@ -126,14 +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(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 f895305f..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,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.UsageDetail; 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(UsageDetail.NAVIGATOR_ITEM); new MainMenu(event.getPlayer()); } } @@ -45,6 +48,7 @@ public void onClick(@NonNull InventoryClickEvent event) { } // Opens the navigator. + FeatureUsage.record(UsageDetail.NAVIGATOR_ITEM); 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..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 @@ -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.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; @@ -118,6 +120,7 @@ public void warpPlayer(Player player, @NotNull Warp warp) { loc.setY(warp.getY()); + FeatureUsage.record(UsageDetail.WARPS_TELEPORT); 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(UsageDetail.WARPS_TELEPORT); // 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(UsageDetail.WARPS_TELEPORT); 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(UsageDetail.WARPS_CREATE); // 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(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 709dc0dd..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 @@ -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.UsageDetail; 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(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/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..489b119c --- /dev/null +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/metrics/FeatureUsageTest.java @@ -0,0 +1,89 @@ +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")); + } + + @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")); + } +}