From 5cadb9bbee6690d1688524df45f3e26b8859b4dd Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 8 Oct 2025 08:41:37 -0700 Subject: [PATCH] feat: analytics --- package.json | 3 ++- pnpm-lock.yaml | 25 ++++++++++++++++----- src/config/progressReminders.ts | 2 +- src/index.ts | 5 +++++ src/modules/logMenteeJoin.ts | 1 + src/modules/logMenteeLeave.ts | 1 + src/modules/processFormSubmission.ts | 2 ++ src/modules/processGitHubEvent.ts | 25 ++++++++++++++++----- src/modules/processMentorshipRole.ts | 12 +++++++--- src/modules/processUserGuildTag.ts | 1 + src/modules/respondToDm.ts | 1 + src/modules/respondToMention.ts | 33 +++++++++++++--------------- 12 files changed, 77 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 2d0e170..09fef60 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "typescript": "5.9.2" }, "dependencies": { - "@nhcarrigan/logger": "1.0.0", + "@nhcarrigan/discord-analytics": "0.0.5", + "@nhcarrigan/logger": "1.1.1", "@retroachievements/api": "2.6.0", "discord.js": "14.22.0", "fastify": "5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b742312..d493f01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@nhcarrigan/discord-analytics': + specifier: 0.0.5 + version: 0.0.5(@nhcarrigan/logger@1.1.1)(discord.js@14.22.0) '@nhcarrigan/logger': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.1.1 + version: 1.1.1 '@retroachievements/api': specifier: 2.6.0 version: 2.6.0 @@ -349,6 +352,12 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@nhcarrigan/discord-analytics@0.0.5': + resolution: {integrity: sha512-dxkXFB/o12AEPWGmv2/d+rQYZ+rzm1tkW9gjeVs/8JMadk+gDIDlSdOlWZrov/VhQGWLpHJFfZD2Y9qcnPf8kg==} + peerDependencies: + '@nhcarrigan/logger': '>=1.1.0-hotfix' + discord.js: ^14.0.0 + '@nhcarrigan/eslint-config@5.2.0': resolution: {integrity: sha512-YpTTqhviKMlRwKF+RC/GYiA5i2jTCmg8uftuiufldneNV5HMbGpTfBbV7tpa8++5mpYJc4+eZaf40QbDiz84dQ==} engines: {node: '>=22', pnpm: '>=9'} @@ -359,8 +368,8 @@ packages: typescript: '>=5' vitest: '>=2' - '@nhcarrigan/logger@1.0.0': - resolution: {integrity: sha512-2e19Bie+ZKb6yKPKjhawqsENkhHatYkvBAmFZx9eToOXdOca+CYi51tldRMtejg6e0+4hOOf2bo5zdBQKmH0dw==} + '@nhcarrigan/logger@1.1.1': + resolution: {integrity: sha512-P6OEQFHDtf6psybYGljuCxkSW6DLQCsx1aZZ3w4YKBXHBFjDbhuvpM9K1kPhVN48hakitx2WPLEoIFr6YZELYw==} '@nhcarrigan/typescript-config@4.0.0': resolution: {integrity: sha512-969HVha7A/Sg77fuMwOm6p14a+7C5iE6g55OD71srqwKIgksQl+Ex/hAI/pyzTQFDQ/FBJbpnHlR4Ov25QV/rw==} @@ -2630,6 +2639,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@nhcarrigan/discord-analytics@0.0.5(@nhcarrigan/logger@1.1.1)(discord.js@14.22.0)': + dependencies: + '@nhcarrigan/logger': 1.1.1 + discord.js: 14.22.0 + node-schedule: 2.1.1 + '@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.40.0(eslint@9.33.0)(typescript@5.9.2))(eslint@9.33.0)(playwright@1.54.2)(react@19.1.1)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0))': dependencies: '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.33.0) @@ -2659,7 +2674,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@nhcarrigan/logger@1.0.0': {} + '@nhcarrigan/logger@1.1.1': {} '@nhcarrigan/typescript-config@4.0.0(typescript@5.9.2)': dependencies: diff --git a/src/config/progressReminders.ts b/src/config/progressReminders.ts index 7aed9f6..d241b78 100644 --- a/src/config/progressReminders.ts +++ b/src/config/progressReminders.ts @@ -24,7 +24,7 @@ const freeCodeCampSprintChannels: Array = [ channelId: "1424801426160488520", createThread: false, name: "fsd sprints", - roleId: "1417979684754428054", + roleId: "1425506225453273224", }, ]; diff --git a/src/index.ts b/src/index.ts index 2824b0d..44821bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ * @author Naomi Carrigan */ +import { DiscordAnalytics } from "@nhcarrigan/discord-analytics"; import { Client, GatewayIntentBits, Events, @@ -59,10 +60,13 @@ const amari: Amari = { recentlyActiveChannels: new Set(), }; +const analytics = new DiscordAnalytics(amari.discord, logger); + amari.discord.once(Events.ClientReady, () => { void logger.log("debug", `Authenticated to Discord as ${amari.discord.user?.username ?? "unknown"}`); void cacheData(amari); + analytics.startCron(); scheduleJob("post news", "0 * * * *", async() => { await postFreeCodeCampNews(amari); await postHackerNews(amari); @@ -91,6 +95,7 @@ amari.discord.on(Events.MessageCreate, (message) => { }); amari.discord.on(Events.InteractionCreate, (interaction) => { + void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); if (interaction.isButton() && interaction.customId === "resolve") { if (interaction.user.id !== ids.users.naomi) { return void interaction.reply( diff --git a/src/modules/logMenteeJoin.ts b/src/modules/logMenteeJoin.ts index b0998cb..4d26952 100644 --- a/src/modules/logMenteeJoin.ts +++ b/src/modules/logMenteeJoin.ts @@ -48,4 +48,5 @@ Welcome to our community! It looks like you may have applied for our mentorship If that is correct, you should ping Naomi to grant your role and begin onboarding! `, }); + await logger.metric("processed_mentee_join", 1, { user: member.id }); }; diff --git a/src/modules/logMenteeLeave.ts b/src/modules/logMenteeLeave.ts index 2bf1b0d..c2d2323 100644 --- a/src/modules/logMenteeLeave.ts +++ b/src/modules/logMenteeLeave.ts @@ -41,4 +41,5 @@ export const logMenteeLeave = async( It seems they were part of the mentorship programme, so you may need to offboard them.`, }); + await logger.metric("processed_mentee_leave", 1, { user: member.id }); }; diff --git a/src/modules/processFormSubmission.ts b/src/modules/processFormSubmission.ts index 88d8009..5d9df15 100644 --- a/src/modules/processFormSubmission.ts +++ b/src/modules/processFormSubmission.ts @@ -18,6 +18,7 @@ import type { FastifyRequest, FastifyReply } from "fastify"; * @param request - The fastify request payload. * @param response - The fastify reply class. */ +// eslint-disable-next-line max-lines-per-function -- only long because of analytics. export const processFormSubmission = async( amari: Amari, // eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify standard. @@ -67,4 +68,5 @@ export const processFormSubmission = async( ], flags: [ MessageFlags.IsComponentsV2 ], }); + await logger.metric("processed_form_submission", 1, { table: String(table) }); }; diff --git a/src/modules/processGitHubEvent.ts b/src/modules/processGitHubEvent.ts index bfcde37..90ffd0a 100644 --- a/src/modules/processGitHubEvent.ts +++ b/src/modules/processGitHubEvent.ts @@ -6,9 +6,11 @@ import { logger } from "../utils/logger.js"; import type { Amari } from "../interfaces/amari.js"; -import type { IssueCreated, +import type { + IssueCreated, PullRequestCreated, - GithubPayload } from "../interfaces/github.js"; + GithubPayload, +} from "../interfaces/github.js"; import type { FastifyRequest, FastifyReply } from "fastify"; const isIssue = (body: GithubPayload): body is IssueCreated => { @@ -44,7 +46,8 @@ export const processGithubEvent = async( } const event = request.headers["x-github-event"]; if (typeof event !== "string") { - await response.status(400). + await response. + status(400). send({ message: "Invalid GitHub event header." }); return; } @@ -57,7 +60,7 @@ export const processGithubEvent = async( if (action === "opened" && event === "issues" && isIssue(request.body)) { await logger.log("info", "Processing new issue"); const { issue, repository } = request.body; - const { number } = issue; + const { number, user } = issue; const { owner, name } = repository; await amari.github.rest.issues.addAssignees({ assignees: [ "naomi-lgbt" ], @@ -66,12 +69,22 @@ export const processGithubEvent = async( owner: owner.login, repo: name, }); + await logger.metric("processed_github_event", 1, { + action: "opened", + event: "issue", + user: user.login, + }); return; } if (action === "opened" && event === "pull_request" && isPull(request.body)) { - await logger.log("info", "Processing new PR"); const { pull_request: pr, repository } = request.body; - const { number } = pr; + const { number, user } = pr; + await logger.log("info", "Processing new PR"); + await logger.metric("processed_github_event", 1, { + action: "opened", + event: "pull_request", + user: user.login, + }); const { owner, name } = repository; await amari.github.rest.pulls.requestReviewers({ owner: owner.login, diff --git a/src/modules/processMentorshipRole.ts b/src/modules/processMentorshipRole.ts index daae341..5d516cd 100644 --- a/src/modules/processMentorshipRole.ts +++ b/src/modules/processMentorshipRole.ts @@ -22,12 +22,15 @@ export const processMentorshipRole = async( oldMember: GuildMember | PartialGuildMember, updatedMember: GuildMember, ): Promise => { - if (oldMember.roles.cache.has(ids.roles.mentorship) - || !updatedMember.roles.cache.has(ids.roles.mentorship)) { + if ( + oldMember.roles.cache.has(ids.roles.mentorship) + || !updatedMember.roles.cache.has(ids.roles.mentorship) + ) { return; } - const channel = amari.discord.channels.cache.get(ids.channels.menteeChat) + const channel + = amari.discord.channels.cache.get(ids.channels.menteeChat) ?? await amari.discord.channels.fetch(ids.channels.menteeChat); if (channel?.isSendable() !== true) { @@ -49,4 +52,7 @@ Once you have done this, your next step is to read our [wiki](`, }); + await logger.metric("processed_mentorship_role", 1, { + user: updatedMember.id, + }); }; diff --git a/src/modules/processUserGuildTag.ts b/src/modules/processUserGuildTag.ts index 789d598..2644ed9 100644 --- a/src/modules/processUserGuildTag.ts +++ b/src/modules/processUserGuildTag.ts @@ -48,6 +48,7 @@ export const processUserGuildTag = async( && member.roles.cache.has(ids.roles.representing)) { await member.roles.remove(ids.roles.representing); } + await logger.metric("processed_guild_tag", 1, { user: user.id }); } catch (error) { if (error instanceof Error) { await logger.error("process user guild tag module", error); diff --git a/src/modules/respondToDm.ts b/src/modules/respondToDm.ts index 051afee..67cc7f8 100644 --- a/src/modules/respondToDm.ts +++ b/src/modules/respondToDm.ts @@ -34,6 +34,7 @@ export const respondToDm = async( await message.reply({ content: responses.dm, }); + await logger.metric("processed_dm", 1, { user: author.id }); } catch (error) { if (error instanceof Error) { await logger.error("respond to DM module", error); diff --git a/src/modules/respondToMention.ts b/src/modules/respondToMention.ts index 50295d9..f2b1124 100644 --- a/src/modules/respondToMention.ts +++ b/src/modules/respondToMention.ts @@ -31,34 +31,31 @@ export const respondToMention = async( if (amari.recentlyActiveChannels.has(channel.id)) { return; } - if (mentions.has(ids.users.naomi, { + const mentionsNaomi = mentions.has(ids.users.naomi, { ignoreEveryone: true, ignoreRepliedUser: true, ignoreRoles: true, - }) || /naomi/i.test(content)) { - await naomi.send( - { - components: getComponentsForNaomi(author, content, url), - flags: [ MessageFlags.IsComponentsV2 ], - }, - ); - return; - } - if (mentions.has(ids.roles.nhcarrigan, { + }) || /naomi/i.test(content); + const mentionsNHCarrigan = mentions.has(ids.roles.nhcarrigan, { ignoreEveryone: true, ignoreRepliedUser: true, }) || mentions.has(ids.users.nhcarrigan, { ignoreEveryone: true, ignoreRepliedUser: true, ignoreRoles: true, - }) || /nhcarrigan/i.test(content)) { - await naomi.send( - { - components: getComponentsForNaomi(author, content, url), - flags: [ MessageFlags.IsComponentsV2 ], - }, - ); + }) || /nhcarrigan/i.test(content); + if (!mentionsNaomi && !mentionsNHCarrigan) { + return; } + await naomi.send( + { + components: getComponentsForNaomi(author, content, url), + flags: [ MessageFlags.IsComponentsV2 ], + }, + ); + await logger.metric("processed_mention", 1, { pingType: mentionsNaomi + ? "naomi" + : "nhcarrigan", user: author.id }); } catch (error) { if (error instanceof Error) { await logger.error("respond to mention module", error);