feat: potential prototype
Node.js CI / Lint and Test (push) Successful in 32s

This commit is contained in:
2025-10-07 17:25:00 -07:00
parent c15566978c
commit 8aabf08555
10 changed files with 4653 additions and 14 deletions
+38
View File
@@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test
+2
View File
@@ -0,0 +1,2 @@
node_modules
prod
+9
View File
@@ -0,0 +1,9 @@
/.gitea/
/.vscode/
eslint.config.js
.gitattributes
.gitignore
/src/
# Ignore packed files so that npm pack can be run locally
*.tgz
+6
View File
@@ -0,0 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"]
}
+4 -14
View File
@@ -1,20 +1,10 @@
# New Repository Template
# Discord Analytics
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository.
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
## Readme
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
<!-- # Project Name
Project Description
This package pairs with our logging tool to pipe some common Discord analytics from all of our bots into our telemetry.
## Live Version
This page is currently deployed. [View the live website.]
This page is currently deployed. [View the live website.](https://www.npmjs.com/package/@nhcarrigan/discord-analytics)
## Feedback and Bugs
@@ -36,4 +26,4 @@ Copyright held by Naomi Carrigan.
## Contact
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. -->
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.
+5
View File
@@ -0,0 +1,5 @@
import NaomisConfig from '@nhcarrigan/eslint-config';
export default [
...NaomisConfig,
]
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@nhcarrigan/discord-analytics",
"version": "0.0.0",
"description": "Package that pairs with our logging tool to provide analytics for our Discord bots.",
"type": "module",
"private": false,
"main": "prod/index.js",
"scripts": {
"lint": "eslint src --max-warnings 0",
"build": "rm -rf prod && tsc",
"test": "echo \"Error: no test specified\" && exit 0"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.18.0",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "24.7.0",
"@types/node-schedule": "2.1.8",
"eslint": "9.37.0",
"typescript": "5.9.3"
},
"peerDependencies": {
"discord.js": "^14.0.0",
"@nhcarrigan/logger": ">=1.1.0-hotfix"
},
"dependencies": {
"node-schedule": "2.1.1"
}
}
+4395
View File
File diff suppressed because it is too large Load Diff
+155
View File
@@ -0,0 +1,155 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Logger } from "@nhcarrigan/logger";
import { scheduleJob, type Job } from "node-schedule";
import type { Events, Client } from "discord.js";
// eslint-disable-next-line complexity, max-lines-per-function, max-statements -- Justified
const flatten = (
object: Record<string, unknown>,
): Record<string, string | number | boolean> => {
const result: Record<string, string | number | boolean> = {};
for (const key in object) {
const value = object[key];
if (value === null || value === undefined) {
continue;
}
if (
typeof value === "string"
|| typeof value === "number"
|| typeof value === "boolean"
) {
result[key] = value;
continue;
}
if (typeof value === "object" && !Array.isArray(value)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Justified
const nested = flatten(value as Record<string, unknown>);
for (const nestedKey in nested) {
const nestedValue = nested[nestedKey];
if (nestedValue === undefined) {
continue;
}
result[`${key}_${nestedKey}`] = nestedValue;
}
continue;
}
if (Array.isArray(value)) {
for (const [ index, arrayValue ] of value.entries()) {
if (
typeof arrayValue === "string"
|| typeof arrayValue === "number"
|| typeof arrayValue === "boolean"
) {
result[`${key}_${index.toString()}`] = arrayValue;
continue;
}
if (typeof arrayValue === "object" && arrayValue !== null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Justified
const nested = flatten(arrayValue as Record<string, unknown>);
for (const nestedKey in nested) {
const nestedValue = nested[nestedKey];
// eslint-disable-next-line max-depth -- Justified
if (nestedValue === undefined) {
continue;
}
result[`${key}_${index.toString()}_${nestedKey}`] = nestedValue;
}
}
}
continue;
}
}
return result;
};
/**
* A class for logging Discord bot analytics.
*/
export class DiscordAnalytics {
private readonly logger: Logger;
private job: Job | null = null;
/**
* Creates a new instance of the DiscordAnalytics class.
* @param client -- The Discord client to monitor.
* @param name -- The name of the application.
* @param logToken -- Auth token for our logging service.
*/
public constructor(
private readonly client: Client,
name: string,
logToken: string,
) {
this.logger = new Logger(name, logToken);
}
/**
* Starts a CRON job to run at midnight (system time) daily.
* This job will fetch all guilds the bot is in, and log the total number of guilds
* and members across all guilds. It will also log the approximate number of user installs
* for the application.
*/
public startCron(): void {
if (this.job) {
return;
}
this.job = scheduleJob("metrics", "0 * * * *", async() => {
try {
const fakeGuilds = await this.client.guilds.fetch();
const guilds = await Promise.all(
fakeGuilds.map(async(guild) => {
return await guild.fetch();
}),
);
const members = guilds.reduce((accumulator, guild) => {
return accumulator + guild.memberCount;
}, 0);
const userInstalls = await this.client.application?.fetch();
await this.logger.metric(
"user_installs",
userInstalls?.approximateUserInstallCount ?? 0,
{},
);
await this.logger.metric("guilds", guilds.length, {});
await this.logger.metric("members", members, {});
} catch (error) {
await this.logger.error(
"Discord Analytics CRON Job",
error instanceof Error
? error
: new Error(String(error)),
);
}
});
}
/**
* Stops the CRON job if it is running.
*/
public stopCron(): void {
if (!this.job) {
return;
}
this.job.cancel();
this.job = null;
}
/**
* Use this to log Discord gateway events. Call this method from your client's event listeners.
* Pass whatever payload you receive from Discord to attach as metadata. Or pass an empty object
* if you don't want to log any metadata.
* @param event - The type of event to log.
* @param payload - The payload to log.
*/
public async logGatewayEvent(
event: Events,
payload: Record<string, unknown>,
): Promise<void> {
await this.logger.metric(event, 1, flatten(payload));
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"outDir": "./prod",
"rootDir": "./src"
},
}