Skip to content

Commit bb7164a

Browse files
authored
Basic Bot Analytics System (#1425)
* Analytics service setup with first use in Ping command * Analytics: remove AnalyticsService injection in PingCommand and using it from BotCore; * feat: update command usage analytics to use generated record methods * Analytics: applies changes for zabuzard 1st CR; * Analytics - Metrics > persist(): rename argument moment to happenedAt * Analytics update - Metrics: renaming persist() to processEvent(); - resources/db: V16 update, removing default value for happened_at column
1 parent c686756 commit bb7164a

File tree

6 files changed

+106
-17
lines changed

6 files changed

+106
-17
lines changed

application/src/main/java/org/togetherjava/tjbot/Application.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.togetherjava.tjbot.db.Database;
1313
import org.togetherjava.tjbot.features.Features;
1414
import org.togetherjava.tjbot.features.SlashCommandAdapter;
15+
import org.togetherjava.tjbot.features.analytics.Metrics;
1516
import org.togetherjava.tjbot.features.system.BotCore;
1617
import org.togetherjava.tjbot.logging.LogMarkers;
1718
import org.togetherjava.tjbot.logging.discord.DiscordLogging;
@@ -82,13 +83,15 @@ public static void runBot(Config config) {
8283
}
8384
Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath());
8485

86+
Metrics metrics = new Metrics(database);
87+
8588
JDA jda = JDABuilder.createDefault(config.getToken())
8689
.enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT)
8790
.build();
8891

8992
jda.awaitReady();
9093

91-
BotCore core = new BotCore(jda, database, config);
94+
BotCore core = new BotCore(jda, database, config, metrics);
9295
CommandReloading.reloadCommands(jda, core);
9396
core.scheduleRoutines(jda);
9497

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.togetherjava.tjbot.config.FeatureBlacklist;
77
import org.togetherjava.tjbot.config.FeatureBlacklistConfig;
88
import org.togetherjava.tjbot.db.Database;
9+
import org.togetherjava.tjbot.features.analytics.Metrics;
910
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
1011
import org.togetherjava.tjbot.features.basic.PingCommand;
1112
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
@@ -90,7 +91,7 @@
9091
* it with the system.
9192
* <p>
9293
* To add a new slash command, extend the commands returned by
93-
* {@link #createFeatures(JDA, Database, Config)}.
94+
* {@link #createFeatures(JDA, Database, Config, Metrics)}.
9495
*/
9596
public class Features {
9697
private Features() {
@@ -106,9 +107,12 @@ private Features() {
106107
* @param jda the JDA instance commands will be registered at
107108
* @param database the database of the application, which features can use to persist data
108109
* @param config the configuration features should use
110+
* @param metrics the metrics service for tracking analytics
109111
* @return a collection of all features
110112
*/
111-
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config) {
113+
@SuppressWarnings("unused")
114+
public static Collection<Feature> createFeatures(JDA jda, Database database, Config config,
115+
Metrics metrics) {
112116
FeatureBlacklistConfig blacklistConfig = config.getFeatureBlacklistConfig();
113117
JShellEval jshellEval = new JShellEval(config.getJshell(), config.getGitHubApiKey());
114118

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.togetherjava.tjbot.features.analytics;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import org.togetherjava.tjbot.db.Database;
7+
import org.togetherjava.tjbot.db.generated.tables.MetricEvents;
8+
9+
import java.time.Instant;
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
13+
/**
14+
* Service for tracking and recording events for analytics purposes.
15+
*/
16+
public final class Metrics {
17+
private static final Logger logger = LoggerFactory.getLogger(Metrics.class);
18+
19+
private final Database database;
20+
21+
private final ExecutorService service = Executors.newSingleThreadExecutor();
22+
23+
/**
24+
* Creates a new instance.
25+
*
26+
* @param database the database to use for storing and retrieving analytics data
27+
*/
28+
public Metrics(Database database) {
29+
this.database = database;
30+
}
31+
32+
/**
33+
* Track an event execution.
34+
*
35+
* @param event the event to save
36+
*/
37+
public void count(String event) {
38+
logger.debug("Counting new record for event: {}", event);
39+
Instant moment = Instant.now();
40+
service.submit(() -> processEvent(event, moment));
41+
42+
}
43+
44+
/**
45+
*
46+
* @param event the event to save
47+
* @param happenedAt the moment when the event is dispatched
48+
*/
49+
private void processEvent(String event, Instant happenedAt) {
50+
database.write(context -> context.newRecord(MetricEvents.METRIC_EVENTS)
51+
.setEvent(event)
52+
.setHappenedAt(happenedAt)
53+
.insert());
54+
}
55+
56+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Analytics system for collecting and persisting bot activity metrics.
3+
* <p>
4+
* This package provides services and components that record events for later analysis and reporting
5+
* across multiple feature areas.
6+
*/
7+
@MethodsReturnNonnullByDefault
8+
@ParametersAreNonnullByDefault
9+
package org.togetherjava.tjbot.features.analytics;
10+
11+
import org.togetherjava.tjbot.annotations.MethodsReturnNonnullByDefault;
12+
13+
import javax.annotation.ParametersAreNonnullByDefault;

application/src/main/java/org/togetherjava/tjbot/features/system/BotCore.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import net.dv8tion.jda.api.hooks.ListenerAdapter;
2424
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
2525
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
26-
import org.jetbrains.annotations.NotNull;
2726
import org.jetbrains.annotations.Nullable;
2827
import org.jetbrains.annotations.Unmodifiable;
2928
import org.slf4j.Logger;
@@ -42,6 +41,7 @@
4241
import org.togetherjava.tjbot.features.UserInteractionType;
4342
import org.togetherjava.tjbot.features.UserInteractor;
4443
import org.togetherjava.tjbot.features.VoiceReceiver;
44+
import org.togetherjava.tjbot.features.analytics.Metrics;
4545
import org.togetherjava.tjbot.features.componentids.ComponentId;
4646
import org.togetherjava.tjbot.features.componentids.ComponentIdParser;
4747
import org.togetherjava.tjbot.features.componentids.ComponentIdStore;
@@ -79,13 +79,13 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
7979
private static final ExecutorService COMMAND_SERVICE = Executors.newCachedThreadPool();
8080
private static final ScheduledExecutorService ROUTINE_SERVICE =
8181
Executors.newScheduledThreadPool(5);
82-
private final Config config;
8382
private final Map<String, UserInteractor> prefixedNameToInteractor;
8483
private final List<Routine> routines;
8584
private final ComponentIdParser componentIdParser;
8685
private final ComponentIdStore componentIdStore;
8786
private final Map<Pattern, MessageReceiver> channelNameToMessageReceiver = new HashMap<>();
8887
private final Map<Pattern, VoiceReceiver> channelNameToVoiceReceiver = new HashMap<>();
88+
private final Metrics metrics;
8989

9090
/**
9191
* Creates a new command system which uses the given database to allow commands to persist data.
@@ -95,10 +95,11 @@ public final class BotCore extends ListenerAdapter implements CommandProvider {
9595
* @param jda the JDA instance that this command system will be used with
9696
* @param database the database that commands may use to persist data
9797
* @param config the configuration to use for this system
98+
* @param metrics the metrics service for tracking analytics
9899
*/
99-
public BotCore(JDA jda, Database database, Config config) {
100-
this.config = config;
101-
Collection<Feature> features = Features.createFeatures(jda, database, config);
100+
public BotCore(JDA jda, Database database, Config config, Metrics metrics) {
101+
this.metrics = metrics;
102+
Collection<Feature> features = Features.createFeatures(jda, database, config, metrics);
102103

103104
// Message receivers
104105
features.stream()
@@ -300,14 +301,14 @@ private Optional<Channel> selectPreferredAudioChannel(@Nullable AudioChannelUnio
300301
}
301302

302303
@Override
303-
public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
304+
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
304305
selectPreferredAudioChannel(event.getChannelJoined(), event.getChannelLeft())
305306
.ifPresent(channel -> getVoiceReceiversSubscribedTo(channel)
306307
.forEach(voiceReceiver -> voiceReceiver.onVoiceUpdate(event)));
307308
}
308309

309310
@Override
310-
public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
311+
public void onGuildVoiceVideo(GuildVoiceVideoEvent event) {
311312
AudioChannelUnion channel = event.getVoiceState().getChannel();
312313

313314
if (channel == null) {
@@ -319,7 +320,7 @@ public void onGuildVoiceVideo(@NotNull GuildVoiceVideoEvent event) {
319320
}
320321

321322
@Override
322-
public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
323+
public void onGuildVoiceStream(GuildVoiceStreamEvent event) {
323324
AudioChannelUnion channel = event.getVoiceState().getChannel();
324325

325326
if (channel == null) {
@@ -331,7 +332,7 @@ public void onGuildVoiceStream(@NotNull GuildVoiceStreamEvent event) {
331332
}
332333

333334
@Override
334-
public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
335+
public void onGuildVoiceMute(GuildVoiceMuteEvent event) {
335336
AudioChannelUnion channel = event.getVoiceState().getChannel();
336337

337338
if (channel == null) {
@@ -343,7 +344,7 @@ public void onGuildVoiceMute(@NotNull GuildVoiceMuteEvent event) {
343344
}
344345

345346
@Override
346-
public void onGuildVoiceDeafen(@NotNull GuildVoiceDeafenEvent event) {
347+
public void onGuildVoiceDeafen(GuildVoiceDeafenEvent event) {
347348
AudioChannelUnion channel = event.getVoiceState().getChannel();
348349

349350
if (channel == null) {
@@ -380,10 +381,16 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
380381

381382
logger.debug("Received slash command '{}' (#{}) on guild '{}'", name, event.getId(),
382383
event.getGuild());
383-
COMMAND_SERVICE.execute(
384-
() -> requireUserInteractor(UserInteractionType.SLASH_COMMAND.getPrefixedName(name),
385-
SlashCommand.class)
386-
.onSlashCommand(event));
384+
COMMAND_SERVICE.execute(() -> {
385+
386+
SlashCommand interactor = requireUserInteractor(
387+
UserInteractionType.SLASH_COMMAND.getPrefixedName(name), SlashCommand.class);
388+
389+
metrics.count("slash-" + name);
390+
391+
interactor.onSlashCommand(event);
392+
393+
});
387394
}
388395

389396
@Override
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE metric_events
2+
(
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
event TEXT NOT NULL,
5+
happened_at TIMESTAMP NOT NULL
6+
);

0 commit comments

Comments
 (0)