Skip to content

Commit 3b7c9e5

Browse files
authored
Tighten up Track output (#183)
This is the new format <img width="550" height="188" alt="Screenshot 2025-09-27 at 10 28 24 PM" src="https://github.com/user-attachments/assets/f4d6613e-5922-46cb-b966-893a97f94308" /> I like it a lot better. It doesn't totally flood the screen when spam hits, which should be a great boon. It still quotes the entirety of the post in the log thread.
1 parent 6db53f1 commit 3b7c9e5

File tree

3 files changed

+143
-112
lines changed

3 files changed

+143
-112
lines changed

app/discord/activityTracker.ts

Lines changed: 13 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
import { Events, ChannelType } from "discord.js";
2-
import type { Client, Message, PartialMessage, TextChannel } from "discord.js";
2+
import type { Client, Message, TextChannel } from "discord.js";
33
import db from "#~/db.server";
4-
import {
5-
parseMarkdownBlocks,
6-
getChars,
7-
getWords,
8-
} from "#~/helpers/messageParsing";
9-
import { partition } from "lodash-es";
104
import { log, trackPerformance } from "#~/helpers/observability";
115
import { threadStats } from "#~/helpers/metrics";
12-
13-
export type CodeStats = {
14-
chars: number;
15-
words: number;
16-
lines: number;
17-
lang: string | undefined;
18-
};
6+
import { getMessageStats } from "#~/helpers/discord.js";
197

208
export async function startActivityTracking(client: Client) {
219
log("info", "ActivityTracker", "Starting activity tracking", {
@@ -100,6 +88,8 @@ export async function startActivityTracking(client: Client) {
10088
.insertInto("message_stats")
10189
.values({
10290
...info,
91+
code_stats: JSON.stringify(info.code_stats),
92+
link_stats: JSON.stringify(info.link_stats),
10393
message_id: msg.id,
10494
author_id: msg.author.id,
10595
guild_id: msg.guildId,
@@ -116,8 +106,8 @@ export async function startActivityTracking(client: Client) {
116106
channelId: msg.channelId,
117107
charCount: info.char_count,
118108
wordCount: info.word_count,
119-
hasCode: info.code_stats !== "[]",
120-
hasLinks: info.link_stats !== "[]",
109+
hasCode: info.code_stats.length > 0,
110+
hasLinks: info.link_stats.length > 0,
121111
});
122112

123113
// Track message in business analytics
@@ -131,7 +121,13 @@ export async function startActivityTracking(client: Client) {
131121
const info = await getMessageStats(msg);
132122
if (!info) return;
133123

134-
await updateStatsById(msg.id).set(info).execute();
124+
await updateStatsById(msg.id)
125+
.set({
126+
...info,
127+
code_stats: JSON.stringify(info.code_stats),
128+
link_stats: JSON.stringify(info.link_stats),
129+
})
130+
.execute();
135131

136132
log("debug", "ActivityTracker", "Message stats updated", {
137133
messageId: msg.id,
@@ -206,75 +202,6 @@ function updateStatsById(id: string) {
206202
return db.updateTable("message_stats").where("message_id", "=", id);
207203
}
208204

209-
async function getMessageStats(msg: Message | PartialMessage) {
210-
return trackPerformance(
211-
"startActivityTracking: getMessageStats",
212-
async () => {
213-
const { content } = await msg.fetch();
214-
215-
const blocks = parseMarkdownBlocks(content);
216-
217-
// TODO: groupBy would be better here, but this was easier to keep typesafe
218-
const [textblocks, nontextblocks] = partition(
219-
blocks,
220-
(b) => b.type === "text",
221-
);
222-
const [links, codeblocks] = partition(
223-
nontextblocks,
224-
(b) => b.type === "link",
225-
);
226-
227-
const linkStats = links.map((link) => link.url);
228-
229-
const { wordCount, charCount } = [...links, ...textblocks].reduce(
230-
(acc, block) => {
231-
const content =
232-
block.type === "link" ? (block.label ?? "") : block.content;
233-
const words = getWords(content).length;
234-
const chars = getChars(content).length;
235-
return {
236-
wordCount: acc.wordCount + words,
237-
charCount: acc.charCount + chars,
238-
};
239-
},
240-
{ wordCount: 0, charCount: 0 },
241-
);
242-
243-
const codeStats = codeblocks.map((block): CodeStats => {
244-
switch (block.type) {
245-
case "fencedcode": {
246-
const content = block.code.join("\n");
247-
return {
248-
chars: getChars(content).length,
249-
words: getWords(content).length,
250-
lines: block.code.length,
251-
lang: block.lang,
252-
};
253-
}
254-
case "inlinecode": {
255-
return {
256-
chars: getChars(block.code).length,
257-
words: getWords(block.code).length,
258-
lines: 1,
259-
lang: undefined,
260-
};
261-
}
262-
}
263-
});
264-
265-
const values = {
266-
char_count: charCount,
267-
word_count: wordCount,
268-
code_stats: JSON.stringify(codeStats),
269-
link_stats: JSON.stringify(linkStats),
270-
react_count: msg.reactions.cache.size,
271-
sent_at: msg.createdTimestamp,
272-
};
273-
return values;
274-
},
275-
);
276-
}
277-
278205
export async function reportByGuild(guildId: string) {
279206
return trackPerformance(
280207
"reportByGuild",

app/helpers/discord.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import {
2+
parseMarkdownBlocks,
3+
getChars,
4+
getWords,
5+
} from "#~/helpers/messageParsing";
6+
import { partition } from "lodash-es";
17
import type {
28
Message,
39
GuildMember,
@@ -21,6 +27,7 @@ import {
2127
SlashCommandBuilder,
2228
} from "discord.js";
2329
import prettyBytes from "pretty-bytes";
30+
import { trackPerformance } from "./observability";
2431

2532
const staffRoles = ["mvp", "moderator", "admin", "admins"];
2633
const helpfulRoles = ["mvp", "star helper"];
@@ -197,3 +204,83 @@ export type ModalCommand = {
197204
export const isModalCommand = (config: AnyCommand): config is ModalCommand =>
198205
"type" in config.command &&
199206
config.command.type === InteractionType.ModalSubmit;
207+
208+
type CodeStats = {
209+
chars: number;
210+
words: number;
211+
lines: number;
212+
lang: string | undefined;
213+
};
214+
/**
215+
* getMessageStats is a helper to retrieve common metrics from a message
216+
* @param msg A Discord Message or PartialMessage object
217+
* @returns { chars: number; words: number; lines: number; lang?: string }
218+
*/
219+
export async function getMessageStats(msg: Message | PartialMessage) {
220+
return trackPerformance(
221+
"startActivityTracking: getMessageStats",
222+
async () => {
223+
const { content } = await msg.fetch();
224+
225+
const blocks = parseMarkdownBlocks(content);
226+
227+
// TODO: groupBy would be better here, but this was easier to keep typesafe
228+
const [textblocks, nontextblocks] = partition(
229+
blocks,
230+
(b) => b.type === "text",
231+
);
232+
const [links, codeblocks] = partition(
233+
nontextblocks,
234+
(b) => b.type === "link",
235+
);
236+
237+
const linkStats = links.map((link) => link.url);
238+
239+
const { wordCount, charCount } = [...links, ...textblocks].reduce(
240+
(acc, block) => {
241+
const content =
242+
block.type === "link" ? (block.label ?? "") : block.content;
243+
const words = getWords(content).length;
244+
const chars = getChars(content).length;
245+
return {
246+
wordCount: acc.wordCount + words,
247+
charCount: acc.charCount + chars,
248+
};
249+
},
250+
{ wordCount: 0, charCount: 0 },
251+
);
252+
253+
const codeStats = codeblocks.map((block): CodeStats => {
254+
switch (block.type) {
255+
case "fencedcode": {
256+
const content = block.code.join("\n");
257+
return {
258+
chars: getChars(content).length,
259+
words: getWords(content).length,
260+
lines: block.code.length,
261+
lang: block.lang,
262+
};
263+
}
264+
case "inlinecode": {
265+
return {
266+
chars: getChars(block.code).length,
267+
words: getWords(block.code).length,
268+
lines: 1,
269+
lang: undefined,
270+
};
271+
}
272+
}
273+
});
274+
275+
const values = {
276+
char_count: charCount,
277+
word_count: wordCount,
278+
code_stats: codeStats,
279+
link_stats: linkStats,
280+
react_count: msg.reactions.cache.size,
281+
sent_at: msg.createdTimestamp,
282+
};
283+
return values;
284+
},
285+
);
286+
}

app/helpers/modLog.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
constructDiscordLink,
2626
describeAttachments,
2727
describeReactions,
28+
getMessageStats,
2829
quoteAndEscape,
2930
quoteAndEscapePoll,
3031
} from "#~/helpers/discord";
@@ -199,23 +200,45 @@ export const reportUser = async ({
199200
staff,
200201
});
201202

203+
// If it has the data for a poll, use a specialized formatting function
204+
const reportedMessage = message.poll
205+
? quoteAndEscapePoll(message.poll)
206+
: quoteAndEscape(message.content).trim();
202207
// Send the detailed log message to thread
203-
const logMessage = await thread.send(logBody);
204-
logMessage.forward(modLog);
208+
const [logMessage] = await Promise.all([
209+
thread.send(logBody),
210+
thread.send(reportedMessage),
211+
]);
205212

206213
// Try to record the report in database
207-
const recordResult = await recordReport({
208-
reportedMessageId: message.id,
209-
reportedChannelId: message.channel.id,
210-
reportedUserId: message.author.id,
211-
guildId: guild.id,
212-
logMessageId: logMessage.id,
213-
logChannelId: thread.id,
214-
reason,
215-
staffId: staff ? staff.id : undefined,
216-
staffUsername: staff ? staff.username : undefined,
217-
extra,
218-
});
214+
const [recordResult] = await Promise.all([
215+
recordReport({
216+
reportedMessageId: message.id,
217+
reportedChannelId: message.channel.id,
218+
reportedUserId: message.author.id,
219+
guildId: guild.id,
220+
logMessageId: logMessage.id,
221+
logChannelId: thread.id,
222+
reason,
223+
staffId: staff ? staff.id : undefined,
224+
staffUsername: staff ? staff.username : undefined,
225+
extra,
226+
}),
227+
logMessage.forward(modLog),
228+
]);
229+
if (thread.parent?.isSendable()) {
230+
const singleLine = message.cleanContent
231+
.slice(0, 50)
232+
.replaceAll("\n", "\\n");
233+
const truncatedMessage =
234+
message.cleanContent.length > 50
235+
? `${singleLine.slice(0, 50)}…`
236+
: singleLine;
237+
const stats = await getMessageStats(message);
238+
await thread.parent.send(
239+
`> ${truncatedMessage}\n-# ${stats.char_count} chars in ${stats.word_count} words. ${stats.link_stats.length} links, ${stats.code_stats.reduce((count, { lines }) => count + lines, 0)} lines of code`,
240+
);
241+
}
219242

220243
// If the record was not inserted due to unique constraint (duplicate),
221244
// this means another process already reported the same message while we were preparing the log.
@@ -283,27 +306,21 @@ const constructLog = async ({
283306
throw new Error("No role configured to be used as moderator");
284307
}
285308

286-
const preface = `<@${lastReport.message.author.id}> (${
287-
lastReport.message.author.username
288-
}) posted ${formatDistanceToNowStrict(lastReport.message.createdAt)} before this log (<t:${Math.floor(lastReport.message.createdTimestamp / 1000)}:R>)`;
289-
const extra = origExtra ? `${origExtra}\n` : "";
290-
291-
// If it has the data for a poll, use a specialized formatting function
292-
const reportedMessage = message.poll
293-
? quoteAndEscapePoll(message.poll)
294-
: quoteAndEscape(message.content).trim();
295-
296309
const { content: report, embeds: reactions = [] } =
297310
makeReportMessage(lastReport);
298311

312+
const preface = `${report} ${constructDiscordLink(message)} by <@${lastReport.message.author.id}> (${
313+
lastReport.message.author.username
314+
})`;
315+
const extra = origExtra ? `${origExtra}\n` : "";
316+
299317
const embeds = [
300318
describeAttachments(message.attachments),
301319
...reactions,
302320
].filter((e): e is APIEmbed => Boolean(e));
303321
return {
304322
content: truncateMessage(`${preface}
305-
${extra}${reportedMessage}
306-
${report} · ${constructDiscordLink(message)}`).trim(),
323+
-# ${extra}${formatDistanceToNowStrict(lastReport.message.createdAt)} · <t:${Math.floor(lastReport.message.createdTimestamp / 1000)}:R> ago`).trim(),
307324
embeds: embeds.length === 0 ? undefined : embeds,
308325
allowedMentions: { roles: [moderator] },
309326
};

0 commit comments

Comments
 (0)