Skip to content

Commit 2cc19b6

Browse files
vcarlclaudeCopilot
authored
Add better observability (#171)
Sentry works now! Also includes a ton of performance tracking and structured logging --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 9398758 commit 2cc19b6

23 files changed

+1628
-359
lines changed

app/commands/force-ban.ts

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,92 @@ import { UserContextMenuCommandInteraction } from "discord.js";
22

33
import { PermissionFlagsBits, ContextMenuCommandBuilder } from "discord.js";
44
import { ApplicationCommandType } from "discord-api-types/v10";
5+
import { log, trackPerformance } from "#~/helpers/observability";
6+
import { commandStats } from "#~/helpers/metrics";
57

6-
export const command = new ContextMenuCommandBuilder()
8+
const command = new ContextMenuCommandBuilder()
79
.setName("Force Ban")
810
.setType(ApplicationCommandType.User)
911
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers);
1012

11-
export const handler = async (
12-
interaction: UserContextMenuCommandInteraction,
13-
) => {
14-
const { targetUser } = interaction;
15-
16-
const { bans } = interaction.guild ?? {};
17-
18-
if (!bans) {
19-
console.error("No guild found on force ban interaction");
20-
await interaction.reply({
21-
ephemeral: true,
22-
content: "Failed to ban user, couldn’t find guild",
23-
});
24-
return;
25-
}
26-
27-
try {
28-
await interaction.guild?.bans.create(targetUser, {
29-
reason: "Force banned by staff",
30-
});
31-
} catch (error) {
32-
console.error("Failed to ban user", error);
33-
await interaction.reply({
34-
ephemeral: true,
35-
content:
36-
"Failed to ban user, try checking the bot's permissions. If they look okay, make sure that the bot’s role is near the top of the roles list — bots can't ban users with roles above their own.",
37-
});
38-
return;
39-
}
40-
await interaction.reply({
41-
ephemeral: true,
42-
content: "This member has been banned",
43-
});
13+
const handler = async (interaction: UserContextMenuCommandInteraction) => {
14+
await trackPerformance(
15+
"forceBanCommand",
16+
async () => {
17+
const { targetUser } = interaction;
18+
19+
log("info", "Commands", "Force ban command executed", {
20+
guildId: interaction.guildId,
21+
moderatorUserId: interaction.user.id,
22+
targetUserId: targetUser.id,
23+
targetUsername: targetUser.username,
24+
});
25+
26+
const { bans } = interaction.guild ?? {};
27+
28+
if (!bans) {
29+
log("error", "Commands", "No guild found on force ban interaction", {
30+
guildId: interaction.guildId,
31+
moderatorUserId: interaction.user.id,
32+
targetUserId: targetUser.id,
33+
});
34+
35+
commandStats.commandFailed(interaction, "force-ban", "No guild found");
36+
37+
await interaction.reply({
38+
ephemeral: true,
39+
content: "Failed to ban user, couldn't find guild",
40+
});
41+
return;
42+
}
43+
44+
try {
45+
await interaction.guild?.bans.create(targetUser, {
46+
reason: "Force banned by staff",
47+
});
48+
49+
log("info", "Commands", "User force banned successfully", {
50+
guildId: interaction.guildId,
51+
moderatorUserId: interaction.user.id,
52+
targetUserId: targetUser.id,
53+
targetUsername: targetUser.username,
54+
reason: "Force banned by staff",
55+
});
56+
57+
commandStats.commandExecuted(interaction, "force-ban", true);
58+
59+
await interaction.reply({
60+
ephemeral: true,
61+
content: "This member has been banned",
62+
});
63+
} catch (error) {
64+
const err = error instanceof Error ? error : new Error(String(error));
65+
66+
log("error", "Commands", "Force ban failed", {
67+
guildId: interaction.guildId,
68+
moderatorUserId: interaction.user.id,
69+
targetUserId: targetUser.id,
70+
targetUsername: targetUser.username,
71+
error: err.message,
72+
stack: err.stack,
73+
});
74+
75+
commandStats.commandFailed(interaction, "force-ban", err.message);
76+
77+
await interaction.reply({
78+
ephemeral: true,
79+
content:
80+
"Failed to ban user, try checking the bot's permissions. If they look okay, make sure that the bot's role is near the top of the roles list — bots can't ban users with roles above their own.",
81+
});
82+
}
83+
},
84+
{
85+
commandName: "force-ban",
86+
guildId: interaction.guildId,
87+
moderatorUserId: interaction.user.id,
88+
targetUserId: interaction.targetUser.id,
89+
},
90+
);
4491
};
92+
93+
export const Command = { handler, command };

app/commands/report.ts

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,84 @@ import { PermissionFlagsBits, ContextMenuCommandBuilder } from "discord.js";
33
import { ApplicationCommandType } from "discord-api-types/v10";
44
import { reportUser } from "#~/helpers/modLog";
55
import { ReportReasons } from "./track/reportCache";
6+
import { log, trackPerformance } from "#~/helpers/observability";
7+
import { commandStats } from "#~/helpers/metrics";
68

7-
export const command = new ContextMenuCommandBuilder()
9+
const command = new ContextMenuCommandBuilder()
810
.setName("Report")
911
.setType(ApplicationCommandType.Message)
1012
.setDefaultMemberPermissions(PermissionFlagsBits.SendMessages);
1113

12-
export const handler = async (
13-
interaction: MessageContextMenuCommandInteraction,
14-
) => {
15-
const { targetMessage: message } = interaction;
16-
17-
await reportUser({
18-
reason: ReportReasons.anonReport,
19-
message,
20-
staff: false,
21-
});
22-
23-
await interaction.reply({
24-
ephemeral: true,
25-
content: "This message has been reported anonymously",
26-
});
14+
const handler = async (interaction: MessageContextMenuCommandInteraction) => {
15+
await trackPerformance(
16+
"reportCommand",
17+
async () => {
18+
const { targetMessage: message } = interaction;
19+
20+
log("info", "Commands", "Report command executed", {
21+
guildId: interaction.guildId,
22+
reporterUserId: interaction.user.id,
23+
targetUserId: message.author?.id,
24+
targetMessageId: message.id,
25+
channelId: interaction.channelId,
26+
});
27+
28+
try {
29+
await reportUser({
30+
reason: ReportReasons.anonReport,
31+
message,
32+
staff: false,
33+
});
34+
35+
log("info", "Commands", "Report submitted successfully", {
36+
guildId: interaction.guildId,
37+
reporterUserId: interaction.user.id,
38+
targetUserId: message.author?.id,
39+
targetMessageId: message.id,
40+
reason: ReportReasons.anonReport,
41+
});
42+
43+
// Track successful report in business analytics
44+
commandStats.reportSubmitted(
45+
interaction,
46+
message.author?.id ?? "unknown",
47+
);
48+
49+
// Track command success
50+
commandStats.commandExecuted(interaction, "report", true);
51+
52+
await interaction.reply({
53+
ephemeral: true,
54+
content: "This message has been reported anonymously",
55+
});
56+
} catch (error) {
57+
const err = error instanceof Error ? error : new Error(String(error));
58+
59+
log("error", "Commands", "Report command failed", {
60+
guildId: interaction.guildId,
61+
reporterUserId: interaction.user.id,
62+
targetUserId: message.author?.id,
63+
targetMessageId: message.id,
64+
error: err.message,
65+
stack: err.stack,
66+
});
67+
68+
// Track command failure in business analytics
69+
commandStats.commandFailed(interaction, "report", err.message);
70+
71+
await interaction.reply({
72+
ephemeral: true,
73+
content: "Failed to submit report. Please try again later.",
74+
});
75+
}
76+
},
77+
{
78+
commandName: "report",
79+
guildId: interaction.guildId,
80+
reporterUserId: interaction.user.id,
81+
targetUserId: interaction.targetMessage.author?.id,
82+
},
83+
);
2784
};
85+
86+
export const Command = { handler, command };

app/commands/setup.ts

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import type { ChatInputCommandInteraction } from "discord.js";
22
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
33

44
import { SETTINGS, setSettings, registerGuild } from "#~/models/guilds.server";
5+
import { log, trackPerformance } from "#~/helpers/observability";
6+
import { commandStats } from "#~/helpers/metrics";
57

6-
export const command = new SlashCommandBuilder()
8+
const command = new SlashCommandBuilder()
79
.setName("setup")
810
.setDescription("Set up necessities for using the bot")
911
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
10-
// TODO: update permissions so non-mods can never use it
11-
// maybe implement as "adder must init immediately"?
12-
// .setDefaultPermission(true);
1312
.addRoleOption((x) =>
1413
x
1514
.setName("moderator")
@@ -30,50 +29,81 @@ export const command = new SlashCommandBuilder()
3029
),
3130
) as SlashCommandBuilder;
3231

33-
export const handler = async (interaction: ChatInputCommandInteraction) => {
34-
try {
35-
if (!interaction.guild) throw new Error("Interaction has no guild");
36-
37-
await registerGuild(interaction.guildId!);
38-
39-
const role = interaction.options.getRole("moderator");
40-
const channel = interaction.options.getChannel("mod-log-channel");
41-
const restricted = interaction.options.getRole("restricted");
42-
if (!role) throw new Error("Interaction has no role");
43-
if (!channel) throw new Error("Interaction has no channel");
44-
45-
await setSettings(interaction.guildId!, {
46-
[SETTINGS.modLog]: channel.id,
47-
[SETTINGS.moderator]: role.id,
48-
[SETTINGS.restricted]: restricted?.id,
49-
});
50-
51-
interaction.reply("Setup completed!");
52-
53-
/*
54-
interaction.followUp({
55-
// tts?: boolean;
56-
// nonce?: string | number;
57-
// content?: string | null;
58-
// embeds?: (MessageEmbed | MessageEmbedOptions | APIEmbed)[];
59-
// components?: (MessageActionRow | (Required<BaseMessageComponentOptions> & MessageActionRowOptions))[];
60-
// allowedMentions?: MessageMentionOptions;
61-
// files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
62-
// attachments?: MessageAttachment[];
63-
64-
// ephemeral?: boolean;
65-
// fetchReply?: boolean;
66-
// threadId?: Snowflake;
67-
}
68-
});
69-
*/
70-
} catch (e) {
71-
if (e instanceof Error) {
72-
interaction.reply(`Something broke:
32+
const handler = async (interaction: ChatInputCommandInteraction) => {
33+
await trackPerformance(
34+
"setupCommand",
35+
async () => {
36+
log("info", "Commands", "Setup command executed", {
37+
guildId: interaction.guildId,
38+
userId: interaction.user.id,
39+
username: interaction.user.username,
40+
});
41+
42+
try {
43+
if (!interaction.guild) throw new Error("Interaction has no guild");
44+
45+
await registerGuild(interaction.guildId!);
46+
47+
const role = interaction.options.getRole("moderator");
48+
const channel = interaction.options.getChannel("mod-log-channel");
49+
const restricted = interaction.options.getRole("restricted");
50+
if (!role) throw new Error("Interaction has no role");
51+
if (!channel) throw new Error("Interaction has no channel");
52+
53+
const settings = {
54+
[SETTINGS.modLog]: channel.id,
55+
[SETTINGS.moderator]: role.id,
56+
[SETTINGS.restricted]: restricted?.id,
57+
};
58+
59+
await setSettings(interaction.guildId!, settings);
60+
61+
log("info", "Commands", "Setup completed successfully", {
62+
guildId: interaction.guildId,
63+
userId: interaction.user.id,
64+
moderatorRoleId: role.id,
65+
modLogChannelId: channel.id,
66+
restrictedRoleId: restricted?.id,
67+
hasRestrictedRole: !!restricted,
68+
});
69+
70+
// Track successful setup in business analytics
71+
commandStats.setupCompleted(interaction, {
72+
moderator: role.id,
73+
modLog: channel.id,
74+
restricted: restricted?.id,
75+
});
76+
77+
// Track command success
78+
commandStats.commandExecuted(interaction, "setup", true);
79+
80+
await interaction.reply("Setup completed!");
81+
} catch (e) {
82+
const error = e instanceof Error ? e : new Error(String(e));
83+
84+
log("error", "Commands", "Setup command failed", {
85+
guildId: interaction.guildId,
86+
userId: interaction.user.id,
87+
error: error.message,
88+
stack: error.stack,
89+
});
90+
91+
// Track command failure in business analytics
92+
commandStats.commandFailed(interaction, "setup", error.message);
93+
94+
await interaction.reply(`Something broke:
7395
\`\`\`
74-
${e.toString()}
96+
${error.toString()}
7597
\`\`\`
7698
`);
77-
}
78-
}
99+
}
100+
},
101+
{
102+
commandName: "setup",
103+
guildId: interaction.guildId,
104+
userId: interaction.user.id,
105+
},
106+
);
79107
};
108+
109+
export const Command = { handler, command };

app/commands/setupTickets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const rest = new REST({ version: "10" }).setToken(discordToken);
3131

3232
const DEFAULT_BUTTON_TEXT = "Open a private ticket with the moderators";
3333

34-
export default [
34+
export const Command = [
3535
{
3636
command: new SlashCommandBuilder()
3737
.setName("tickets-channel")

0 commit comments

Comments
 (0)