Skip to content

Commit 0682838

Browse files
authored
Add support for dimensions to metrics (#1447)
1 parent 97801ad commit 0682838

File tree

10 files changed

+108
-17
lines changed

10 files changed

+108
-17
lines changed

PP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ In certain circumstances, you have the following data protection rights:
4444

4545
The databases may store
4646
* `user_id` of users (the unique id of a Discord account),
47+
* `user_name` of users (the username of a Discord account), stored as part of metric event dimensions when tracking command usage (e.g. slash commands, tag lookups),
4748
* `timestamp`s of actions (for example when a command has been used),
4849
* `guild_id` of guilds the **bot** is member of (the unique id of a Discord guild),
4950
* `channel_id` of channels belonging to guilds the **bot** is member of (the unique id of a Discord channel),
5051
* `message_id` of messages send by users in guilds the **bot** is member of (the unique id of a Discord message),
5152
* `participant_count` of no of people who participated in help thread discussions,
5253
* `tags` aka categories to which these help threads belong to,
5354
* `timestamp`s for both when thread was created and closed,
54-
* `message_count` the no of messages that were sent in lifecycle of any help thread
55+
* `message_count` the no of messages that were sent in lifecycle of any help thread,
56+
* `dimensions` optional JSON metadata attached to metric events, which may include the `user_name` of the user who triggered the event and contextual details such as the command name or tag id
5557

5658
_Note: Help threads are just threads that are created via forum channels, used for anyone to ask questions and get help
5759
in certain problems._

application/src/main/java/org/togetherjava/tjbot/features/analytics/EmojiTrackerListener.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import org.togetherjava.tjbot.features.MessageReceiverAdapter;
1010

11+
import java.util.Map;
12+
1113
/**
1214
* Listener that tracks custom emoji usage across all channels for analytics purposes.
1315
* <p>
@@ -18,6 +20,7 @@
1820
* custom emojis are tracked separately (e.g. {@code emoji-custom-animated-123456789}).
1921
*/
2022
public final class EmojiTrackerListener extends MessageReceiverAdapter {
23+
private static final String METRIC_NAME = "emoji";
2124
private final Metrics metrics;
2225

2326
/**
@@ -37,7 +40,11 @@ public void onMessageReceived(MessageReceivedEvent event) {
3740
return;
3841
}
3942

40-
event.getMessage().getMentions().getCustomEmojis().forEach(this::trackCustomEmoji);
43+
event.getMessage()
44+
.getMentions()
45+
.getCustomEmojis()
46+
.forEach(customEmoji -> trackCustomEmoji("message", customEmoji.getIdLong(),
47+
customEmoji.isAnimated()));
4148
}
4249

4350
@Override
@@ -47,11 +54,12 @@ public void onMessageReactionAdd(MessageReactionAddEvent event) {
4754
return;
4855
}
4956

50-
trackCustomEmoji(emoji.asCustom());
57+
CustomEmoji customEmoji = emoji.asCustom();
58+
59+
trackCustomEmoji("reaction", customEmoji.getIdLong(), customEmoji.isAnimated());
5160
}
5261

53-
private void trackCustomEmoji(CustomEmoji emoji) {
54-
String prefix = emoji.isAnimated() ? "emoji-custom-animated-" : "emoji-custom-";
55-
metrics.count(prefix + emoji.getIdLong());
62+
private void trackCustomEmoji(String type, long id, boolean isAnimated) {
63+
metrics.count(METRIC_NAME, Map.of("type", type, "id", id, "animated", isAnimated));
5664
}
5765
}

application/src/main/java/org/togetherjava/tjbot/features/analytics/Metrics.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package org.togetherjava.tjbot.features.analytics;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
35
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
57

68
import org.togetherjava.tjbot.db.Database;
79
import org.togetherjava.tjbot.db.generated.tables.MetricEvents;
810

11+
import javax.annotation.Nullable;
12+
913
import java.time.Instant;
14+
import java.util.Map;
1015
import java.util.concurrent.ExecutorService;
1116
import java.util.concurrent.Executors;
1217

@@ -15,6 +20,7 @@
1520
*/
1621
public final class Metrics {
1722
private static final Logger logger = LoggerFactory.getLogger(Metrics.class);
23+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
1824

1925
private final Database database;
2026

@@ -35,21 +41,48 @@ public Metrics(Database database) {
3541
* @param event the event to save
3642
*/
3743
public void count(String event) {
44+
count(event, Map.of());
45+
}
46+
47+
/**
48+
* Track an event execution with additional contextual data.
49+
*
50+
* @param event the name of the event to record (e.g. "user_signup", "purchase")
51+
* @param dimensions optional key-value pairs providing extra context about the event. These are
52+
* often referred to as "metadata" and can include things like: userId: "12345", name:
53+
* "John Smith", channel_name: "chit-chat" etc. This data helps with filtering, grouping,
54+
* and analyzing events later. Note: A value for a metric should be a Java primitive
55+
* (String, int, double, long float).
56+
*/
57+
public void count(String event, Map<String, Object> dimensions) {
3858
logger.debug("Counting new record for event: {}", event);
39-
Instant moment = Instant.now();
40-
service.submit(() -> processEvent(event, moment));
4159

60+
Instant happenedAt = Instant.now();
61+
String serializedDimensions = serializeDimensions(dimensions);
62+
63+
service.submit(() -> processEvent(event, happenedAt,
64+
dimensions.isEmpty() ? null : serializedDimensions));
65+
}
66+
67+
private static String serializeDimensions(Map<String, Object> dimensions) {
68+
try {
69+
return OBJECT_MAPPER.writeValueAsString(dimensions);
70+
} catch (JsonProcessingException e) {
71+
throw new IllegalArgumentException("Failed to serialize dimensions", e);
72+
}
4273
}
4374

4475
/**
4576
*
4677
* @param event the event to save
4778
* @param happenedAt the moment when the event is dispatched
79+
* @param dimensionsJson optional JSON-serialized dimensions, or null
4880
*/
49-
private void processEvent(String event, Instant happenedAt) {
81+
private void processEvent(String event, Instant happenedAt, @Nullable String dimensionsJson) {
5082
database.write(context -> context.newRecord(MetricEvents.METRIC_EVENTS)
5183
.setEvent(event)
5284
.setHappenedAt(happenedAt)
85+
.setDimensions(dimensionsJson)
5386
.insert());
5487
}
5588

application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
188188
CodeFence code = extractCodeOrFallback(originalMessage.get().getContentRaw());
189189

190190
// Apply the selected action
191-
metrics.count("code_action-" + codeAction.getLabel());
191+
metrics.count("code_action", Map.of("name", codeAction.getLabel()));
192192
return event.getHook()
193193
.editOriginalEmbeds(codeAction.apply(code))
194194
.setActionRow(createButtons(originalMessageId, codeAction));

application/src/main/java/org/togetherjava/tjbot/features/help/AutoPruneHelperRoutine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ private void pruneRoleIfFull(List<Member> members, Role targetRole,
112112

113113
if (isRoleFull(withRole)) {
114114
logger.debug("Helper role {} is full, starting to prune.", targetRole.getName());
115-
metrics.count("autoprune_helper-" + targetRole.getName());
115+
metrics.count("autoprune_helper", Map.of("role", targetRole.getName()));
116116
pruneRole(targetRole, withRole, selectRoleChannel, when);
117117
}
118118
}

application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ private void changeCategory(SlashCommandInteractionEvent event, ThreadChannel he
162162
event.deferReply().queue();
163163
refreshCooldownFor(Subcommand.CHANGE_CATEGORY, helpThread);
164164

165-
metrics.count("help-category-" + category);
165+
metrics.count("help-category", Map.of(CHANGE_CATEGORY_SUBCOMMAND, category));
166166
helper.changeChannelCategory(helpThread, category)
167167
.flatMap(_ -> sendCategoryChangedMessage(helpThread.getGuild(), event.getHook(),
168168
helpThread, category))

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,16 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
385385
SlashCommand interactor = requireUserInteractor(
386386
UserInteractionType.SLASH_COMMAND.getPrefixedName(name), SlashCommand.class);
387387

388-
String eventName = "slash-" + name;
388+
Map<String, Object> dimensions = new HashMap<>();
389+
dimensions.put("name", name);
390+
dimensions.put("user", event.getUser().getName());
391+
dimensions.put("userId", event.getUser().getIdLong());
392+
389393
if (event.getSubcommandName() != null) {
390-
eventName += "_" + event.getSubcommandName();
394+
dimensions.put("subCommandName", event.getSubcommandName());
391395
}
392-
metrics.count(eventName);
396+
397+
metrics.count("slash", dimensions);
393398

394399
interactor.onSlashCommand(event);
395400
});

application/src/main/java/org/togetherjava/tjbot/features/tags/TagCommand.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.ArrayList;
2929
import java.util.Collection;
3030
import java.util.List;
31+
import java.util.Map;
3132
import java.util.Objects;
3233
import java.util.Optional;
3334
import java.util.Set;
@@ -91,7 +92,8 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
9192
if (tagSystem.handleIsUnknownTag(id, event)) {
9293
return;
9394
}
94-
metrics.count("tag-" + id);
95+
metrics.count("tag", Map.of("id", id, "user", event.getUser().getName(), "userId",
96+
event.getUser().getIdLong()));
9597

9698
String tagContent = tagSystem.getTag(id).orElseThrow();
9799
MessageEmbed contentEmbed = new EmbedBuilder().setDescription(tagContent)

application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersAssignmentRoutine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ private void manageTopHelperRole(Collection<? extends Member> currentTopHelpers,
260260
}
261261

262262
for (long topHelperUserId : selectedTopHelperIds) {
263-
metrics.count("top_helper-" + topHelperUserId);
263+
metrics.count("top_helper", Map.of("userId", topHelperUserId));
264264
}
265265
reportRoleManageSuccess(event);
266266
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
ALTER TABLE metric_events ADD COLUMN dimensions TEXT;
2+
3+
UPDATE metric_events
4+
SET event = 'code_action',
5+
dimensions = json_object('name', SUBSTR(event, LENGTH('code_action-') + 1))
6+
WHERE event LIKE 'code_action-%';
7+
8+
UPDATE metric_events
9+
SET event = 'autoprune_helper',
10+
dimensions = json_object('role', SUBSTR(event, LENGTH('autoprune_helper-') + 1))
11+
WHERE event LIKE 'autoprune_helper-%';
12+
13+
UPDATE metric_events
14+
SET event = 'help-category',
15+
dimensions = json_object('category', SUBSTR(event, LENGTH('help-category-') + 1))
16+
WHERE event LIKE 'help-category-%';
17+
18+
UPDATE metric_events
19+
SET event = 'tag',
20+
dimensions = json_object('id', SUBSTR(event, LENGTH('tag-') + 1))
21+
WHERE event LIKE 'tag-%';
22+
23+
UPDATE metric_events
24+
SET event = 'top_helper',
25+
dimensions = json_object('userId', CAST(SUBSTR(event, LENGTH('top_helper-') + 1) AS INTEGER))
26+
WHERE event LIKE 'top_helper-%';
27+
28+
UPDATE metric_events
29+
SET event = 'slash',
30+
dimensions = json_object(
31+
'name', SUBSTR(event, LENGTH('slash-') + 1, INSTR(SUBSTR(event, LENGTH('slash-') + 1), '_') - 1),
32+
'subCommandName', SUBSTR(event, LENGTH('slash-') + 1 + INSTR(SUBSTR(event, LENGTH('slash-') + 1), '_'))
33+
)
34+
WHERE event LIKE 'slash-%'
35+
AND INSTR(SUBSTR(event, LENGTH('slash-') + 1), '_') > 0;
36+
37+
UPDATE metric_events
38+
SET event = 'slash',
39+
dimensions = json_object('name', SUBSTR(event, LENGTH('slash-') + 1))
40+
WHERE event LIKE 'slash-%'
41+
AND INSTR(SUBSTR(event, LENGTH('slash-') + 1), '_') = 0;

0 commit comments

Comments
 (0)