Skip to content

Commit 8774945

Browse files
committed
Analytics service setup with first use in Ping command
1 parent 1070f23 commit 8774945

File tree

6 files changed

+144
-4
lines changed

6 files changed

+144
-4
lines changed

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

Lines changed: 3 additions & 1 deletion
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.AnalyticsService;
910
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
1011
import org.togetherjava.tjbot.features.basic.PingCommand;
1112
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
@@ -128,6 +129,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
128129
TopHelpersService topHelpersService = new TopHelpersService(database);
129130
TopHelpersAssignmentRoutine topHelpersAssignmentRoutine =
130131
new TopHelpersAssignmentRoutine(config, topHelpersService);
132+
AnalyticsService analyticsService = new AnalyticsService(database);
131133

132134
// NOTE The system can add special system relevant commands also by itself,
133135
// hence this list may not necessarily represent the full list of all commands actually
@@ -184,7 +186,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
184186

185187
// Slash commands
186188
features.add(new LogLevelCommand());
187-
features.add(new PingCommand());
189+
features.add(new PingCommand(analyticsService));
188190
features.add(new TeXCommand());
189191
features.add(new TagCommand(tagSystem));
190192
features.add(new TagManageCommand(tagSystem, modAuditLogWriter));
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.togetherjava.tjbot.features.analytics;
2+
3+
import org.jetbrains.annotations.Nullable;
4+
import org.jooq.impl.DSL;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import org.togetherjava.tjbot.db.Database;
9+
10+
/**
11+
* Service for tracking and recording command usage analytics.
12+
* <p>
13+
* This service records every command execution along with its success/failure status, allowing for
14+
* analysis of command usage patterns, popular commands, error rates, and activity over time.
15+
* <p>
16+
* Commands should call {@link #recordCommandExecution(long, String, long, boolean, String)} after
17+
* execution to track their usage.
18+
*/
19+
public final class AnalyticsService {
20+
private static final Logger logger = LoggerFactory.getLogger(AnalyticsService.class);
21+
22+
private final Database database;
23+
24+
/**
25+
* Creates a new instance.
26+
*
27+
* @param database the database to use for storing and retrieving analytics data
28+
*/
29+
public AnalyticsService(Database database) {
30+
this.database = database;
31+
}
32+
33+
/**
34+
* Records a command execution with success/failure status.
35+
* <p>
36+
* This method should be called by commands after they complete execution to track usage
37+
* patterns and error rates.
38+
*
39+
* @param guildId the guild ID where the command was executed
40+
* @param commandName the name of the command that was executed
41+
* @param userId the ID of the user who executed the command
42+
* @param success whether the command executed successfully
43+
* @param errorMessage optional error message if the command failed (null if successful)
44+
*/
45+
public void recordCommandExecution(long guildId, String commandName, long userId,
46+
boolean success, @Nullable String errorMessage) {
47+
48+
database.write(context -> context
49+
.insertInto(DSL.table("command_usage"), DSL.field("guild_id"),
50+
DSL.field("command_name"), DSL.field("user_id"), DSL.field("executed_at"),
51+
DSL.field("success"), DSL.field("error_message"))
52+
.values(guildId, commandName, userId, DSL.currentTimestamp(), success, errorMessage)
53+
.execute());
54+
55+
if (!success && errorMessage != null) {
56+
logger.warn("Command '{}' failed in guild {} with error: {}", commandName, guildId,
57+
errorMessage);
58+
}
59+
}
60+
61+
/**
62+
* Records a successful command execution.
63+
*
64+
* @param guildId the guild ID where the command was executed
65+
* @param commandName the name of the command that was executed
66+
* @param userId the ID of the user who executed the command
67+
*/
68+
public void recordCommandSuccess(long guildId, String commandName, long userId) {
69+
recordCommandExecution(guildId, commandName, userId, true, null);
70+
}
71+
72+
/**
73+
* Records a failed command execution.
74+
*
75+
* @param guildId the guild ID where the command was executed
76+
* @param commandName the name of the command that was executed
77+
* @param userId the ID of the user who executed the command
78+
* @param errorMessage a description of what went wrong
79+
*/
80+
public void recordCommandFailure(long guildId, String commandName, long userId,
81+
String errorMessage) {
82+
recordCommandExecution(guildId, commandName, userId, false, errorMessage);
83+
}
84+
}
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, not limited to commands.
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;
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
package org.togetherjava.tjbot.features.basic;
22

3+
import net.dv8tion.jda.api.entities.Guild;
34
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
45

56
import org.togetherjava.tjbot.features.CommandVisibility;
67
import org.togetherjava.tjbot.features.SlashCommandAdapter;
8+
import org.togetherjava.tjbot.features.analytics.AnalyticsService;
79

810
/**
911
* Implementation of an example command to illustrate how to respond to a user.
1012
* <p>
1113
* The implemented command is {@code /ping}, upon which the bot will respond with {@code Pong!}.
1214
*/
1315
public final class PingCommand extends SlashCommandAdapter {
16+
private final AnalyticsService analyticsService;
17+
1418
/**
1519
* Creates an instance of the ping pong command.
20+
*
21+
* @param analyticsService the analytics service to track command usage
1622
*/
17-
public PingCommand() {
23+
public PingCommand(AnalyticsService analyticsService) {
1824
super("ping", "Bot responds with 'Pong!'", CommandVisibility.GUILD);
25+
this.analyticsService = analyticsService;
1926
}
2027

2128
/**
@@ -25,6 +32,23 @@ public PingCommand() {
2532
*/
2633
@Override
2734
public void onSlashCommand(SlashCommandInteractionEvent event) {
28-
event.reply("Pong!").queue();
35+
Guild guild = event.getGuild();
36+
if (guild == null) {
37+
event.reply("This command can only be used in a server!").setEphemeral(true).queue();
38+
return;
39+
}
40+
41+
try {
42+
event.reply("Pong!").queue();
43+
44+
analyticsService.recordCommandSuccess(guild.getIdLong(), getName(),
45+
event.getUser().getIdLong());
46+
47+
} catch (Exception e) {
48+
analyticsService.recordCommandFailure(guild.getIdLong(), getName(),
49+
event.getUser().getIdLong(),
50+
e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
51+
throw e;
52+
}
2953
}
3054
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CREATE TABLE command_usage
2+
(
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
guild_id INTEGER NOT NULL,
5+
command_name TEXT NOT NULL,
6+
user_id INTEGER NOT NULL,
7+
executed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
success BOOLEAN NOT NULL DEFAULT TRUE,
9+
error_message TEXT
10+
);
11+
12+
CREATE INDEX idx_command_usage_guild ON command_usage(guild_id);
13+
CREATE INDEX idx_command_usage_command_name ON command_usage(command_name);

application/src/test/java/org/togetherjava/tjbot/features/basic/PingCommandTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
import org.junit.jupiter.api.Test;
77

88
import org.togetherjava.tjbot.features.SlashCommand;
9+
import org.togetherjava.tjbot.features.analytics.AnalyticsService;
910
import org.togetherjava.tjbot.jda.JdaTester;
1011

12+
import static org.mockito.Mockito.mock;
1113
import static org.mockito.Mockito.verify;
1214

1315
final class PingCommandTest {
1416
private JdaTester jdaTester;
1517
private SlashCommand command;
18+
private AnalyticsService analyticsService;
1619

1720
private SlashCommandInteractionEvent triggerSlashCommand() {
1821
SlashCommandInteractionEvent event =
@@ -24,7 +27,8 @@ private SlashCommandInteractionEvent triggerSlashCommand() {
2427
@BeforeEach
2528
void setUp() {
2629
jdaTester = new JdaTester();
27-
command = jdaTester.spySlashCommand(new PingCommand());
30+
analyticsService = mock(AnalyticsService.class);
31+
command = jdaTester.spySlashCommand(new PingCommand(analyticsService));
2832
}
2933

3034
@Test

0 commit comments

Comments
 (0)