generated from nhcarrigan/template
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
prod
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/.gitea/
|
||||||
|
/.vscode/
|
||||||
|
eslint.config.js
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
/src/
|
||||||
|
|
||||||
|
# Ignore packed files so that npm pack can be run locally
|
||||||
|
*.tgz
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": ["typescript"]
|
||||||
|
}
|
||||||
@@ -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.
|
This package pairs with our logging tool to pipe some common Discord analytics from all of our bots into our telemetry.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## Live Version
|
## 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
|
## Feedback and Bugs
|
||||||
|
|
||||||
@@ -36,4 +26,4 @@ Copyright held by Naomi Carrigan.
|
|||||||
|
|
||||||
## Contact
|
## 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`.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import NaomisConfig from '@nhcarrigan/eslint-config';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...NaomisConfig,
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+4395
File diff suppressed because it is too large
Load Diff
+155
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "@nhcarrigan/typescript-config",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./prod",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user