diff --git a/config.example.json b/config.example.json index df3c59d8..db778ac6 100644 --- a/config.example.json +++ b/config.example.json @@ -33,5 +33,8 @@ "lilacWebsocket": "ws://localhost:4000/socket", "lilacPassword": "this needs to match the 'password' config in Lilac", - "redisHost": "localhost" + "redisHost": "localhost", + + "supernovaURL": "http://localhost:8082/", + "supernovaPassword": "this needs to match the 'password' config in Supernova" } diff --git a/src/errors/lilac.ts b/src/errors/lilac.ts index 4e61086d..1fd25898 100644 --- a/src/errors/lilac.ts +++ b/src/errors/lilac.ts @@ -1,8 +1,20 @@ +import { ApolloError, ServerError } from "@apollo/client"; +import { ErrorWithSupernovaID } from "../services/analytics/ReportingService"; import { ClientError } from "./errors"; export function parseLilacError(error: Error): Error { if (error.message === "User is already being indexed or updated!") { return new AlreadyBeingUpdatedOrIndexedError(); + } else if ( + error instanceof ApolloError && + isServerError(error.networkError) + ) { + if (error.networkError.result.supernova_id) { + return new LilacError( + error.message, + error.networkError.result.supernova_id + ); + } } return error; @@ -15,3 +27,14 @@ export class AlreadyBeingUpdatedOrIndexedError extends ClientError { ); } } + +export class LilacError extends Error implements ErrorWithSupernovaID { + constructor(message: string, public readonly supernovaID: string) { + super(message); + this.name = "LilacError"; + } +} + +export function isServerError(error: any | null): error is ServerError { + return !!(error as ServerError)?.response; +} diff --git a/src/index.ts b/src/index.ts index 87053cda..0140c894 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ import { GowonContext } from "./lib/context/Context"; import { Payload } from "./lib/context/Payload"; import { displayUserTag } from "./lib/ui/displays"; import { MockMessage } from "./mocks/discord"; -import { ReportingService } from "./services/analytics/ReportingService"; import { UsersService } from "./services/dbservices/UsersService"; import { client, @@ -38,9 +37,6 @@ async function start() { const analyticsCollector = ServiceRegistry.get(AnalyticsCollector); const usersService = ServiceRegistry.get(UsersService); - const reportingService = ServiceRegistry.get(ReportingService); - - reportingService.init(); client.client.on("ready", () => { console.log( diff --git a/src/lib/command/Command.ts b/src/lib/command/Command.ts index f7311981..a706cf1c 100644 --- a/src/lib/command/Command.ts +++ b/src/lib/command/Command.ts @@ -15,7 +15,7 @@ import { GowonService } from "../../services/GowonService"; import { NowPlayingEmbedParsingService } from "../../services/NowPlayingEmbedParsingService"; import { ServiceRegistry } from "../../services/ServicesRegistry"; import { TrackingService } from "../../services/TrackingService"; -import { ReportingService } from "../../services/analytics/ReportingService"; +import { ErrorReportingService } from "../../services/analytics/ReportingService"; import { ArgumentParsingService } from "../../services/arguments/ArgumentsParsingService"; import { MentionsService } from "../../services/arguments/mentions/MentionsService"; import { @@ -204,7 +204,7 @@ export abstract class Command { discordService = ServiceRegistry.get(DiscordService); settingsService = ServiceRegistry.get(SettingsService); mentionsService = ServiceRegistry.get(MentionsService); - reportingService = ServiceRegistry.get(ReportingService); + reportingService = ServiceRegistry.get(ErrorReportingService); mirrorballService = ServiceRegistry.get(MirrorballService); lilacUsersService = ServiceRegistry.get(LilacUsersService); lilacGuildsService = ServiceRegistry.get(LilacGuildsService); @@ -347,14 +347,12 @@ export abstract class Command { protected async handleRunError(e: any) { this.logger.logError(e); this.analyticsCollector.metrics.commandErrors.inc(); - this.reportingService.reportError(this.ctx, e); - - console.log(e); + const errorID = await this.reportingService.reportError(this.ctx, e); if (e.isClientFacing && !e.silent) { await this.sendError(e); } else if (!e.isClientFacing) { - await this.sendError(new UnknownError()); + await this.sendError(new UnknownError(), errorID); } } @@ -462,11 +460,16 @@ export abstract class Command { .filter(filter); } - protected async sendError(error: Error | string): Promise { + protected async sendError( + error: Error | string, + errorID?: string + ): Promise { const errorInstance = typeof error === "string" ? new ClientError(error) : error; - const embed = new ErrorEmbed().setError(errorInstance); + const embed = new ErrorEmbed() + .setError(errorInstance) + .setErrorCode(errorID); await this.reply(embed); } diff --git a/src/lib/ui/embeds/AquariumEmbed.ts b/src/lib/ui/embeds/AquariumEmbed.ts index c591470e..bed64869 100644 --- a/src/lib/ui/embeds/AquariumEmbed.ts +++ b/src/lib/ui/embeds/AquariumEmbed.ts @@ -250,7 +250,6 @@ There be **${displayNumber(this.aquarium.size)} total fishy** in your aquarium. "Your fishy are hard at work doing something", "The fishy have become aware they are in a Discord bot. your time is limited.", "Your fishy have hacked the mainframe", - "Your fishy are holding a prayer circle to get Leoni a legendary", "Your fishy are having a heated debate about food... again", `The fishy are starting a multilevel marketing scheme ${emDash} want to buy a pyramidfish?`, "The fishy are pondering the existence of the 'other side' of the glass.", diff --git a/src/lib/ui/embeds/ErrorEmbed.ts b/src/lib/ui/embeds/ErrorEmbed.ts index 4f02623a..f94e6b6b 100644 --- a/src/lib/ui/embeds/ErrorEmbed.ts +++ b/src/lib/ui/embeds/ErrorEmbed.ts @@ -8,6 +8,7 @@ export const errorColour = "#F1759A"; export class ErrorEmbed extends EmbedView { error?: Error; + errorCode?: string; asDiscordSendable(): EmbedView { const footer = this.error instanceof ClientError ? this.error.footer : ""; @@ -21,6 +22,8 @@ export class ErrorEmbed extends EmbedView { if (footer) { embed.setFooter(footer); + } else if (this.errorCode) { + embed.setFooter(`Error ID: ${this.errorCode}`); } return isWarning ? embed.convert(WarningEmbed) : embed; @@ -31,6 +34,11 @@ export class ErrorEmbed extends EmbedView { return this; } + setErrorCode(code: string | undefined): this { + this.errorCode = code; + return this; + } + protected description(): string | undefined { if (this.error) { return uppercaseFirstLetter(this.error.message); diff --git a/src/services/ServicesRegistry.ts b/src/services/ServicesRegistry.ts index 7cd990bc..d76053a1 100644 --- a/src/services/ServicesRegistry.ts +++ b/src/services/ServicesRegistry.ts @@ -23,7 +23,7 @@ import { SpotifyService } from "./Spotify/SpotifyService"; import { TimeAndDateService } from "./TimeAndDateService"; import { TrackingService } from "./TrackingService"; import { WordBlacklistService } from "./WordBlacklistService"; -import { ReportingService } from "./analytics/ReportingService"; +import { ErrorReportingService } from "./analytics/ReportingService"; import { ArgumentParsingService } from "./arguments/ArgumentsParsingService"; import { MentionsService } from "./arguments/mentions/MentionsService"; import { BotStatsService } from "./dbservices/BotStatsService"; @@ -108,7 +108,7 @@ const services: Service[] = [ RedirectsService, RedisService, RedisInteractionService, - ReportingService, + ErrorReportingService, SettingsService, SpotifyService, SpotifyArguments, diff --git a/src/services/analytics/ReportingService.ts b/src/services/analytics/ReportingService.ts index f7f609c4..e7d74339 100644 --- a/src/services/analytics/ReportingService.ts +++ b/src/services/analytics/ReportingService.ts @@ -1,8 +1,101 @@ +import { supernovaPassword, supernovaURL } from "../../../config.json"; +import { ClientError, ClientWarning } from "../../errors/errors"; import { GowonContext } from "../../lib/context/Context"; import { BaseService } from "../BaseService"; +import { + ErrorReportPayload, + GowonErrorSeverity, +} from "../supernova/supernovaTypes"; -export class ReportingService extends BaseService { - async init() {} +export type ErrorWithSupernovaID = Error & { + supernovaID: string; +}; - public reportError(_ctx: GowonContext, _e: Error): void {} +export class ErrorReportingService extends BaseService { + public async reportError( + ctx: GowonContext, + e: Error + ): Promise { + if (this.alreadyReportedToSupernova(e)) { + await this.fetch( + `${e.supernovaID}/modify`, + "POST", + this.generateModifyPayload(ctx) + ); + + return e.supernovaID; + } + + try { + const response = await this.fetch( + "report", + "POST", + this.generatePayload(ctx, e) + ); + + const { error } = await response.json(); + + return error.id; + } catch { + return undefined; + } + } + + private generatePayload(ctx: GowonContext, e: Error): ErrorReportPayload { + return { + kind: e.name, + message: e.message, + application: "Gowon", + userID: ctx.author.id, + severity: this.getSeverity(e), + stack: e.stack ?? "(no stack)", + tags: this.generateTags(ctx), + }; + } + + private generateModifyPayload( + ctx: GowonContext + ): Partial { + return { + userID: ctx.author.id, + tags: this.generateTags(ctx), + }; + } + + private generateTags(ctx: GowonContext): ErrorReportPayload["tags"] { + return [ + { key: "command", value: ctx.command.name }, + { key: "guild", value: ctx.guild?.id ?? "DM" }, + { key: "runas", value: ctx.extract.matched }, + ]; + } + + private getSeverity(e: Error): GowonErrorSeverity { + return e instanceof ClientWarning + ? GowonErrorSeverity.WARNING + : e instanceof ClientError + ? GowonErrorSeverity.EXCEPTION + : GowonErrorSeverity.ERROR; + } + + private getURL(path: string): string { + return supernovaURL + "api/errors/" + path; + } + + private async fetch(path: string, method: "GET" | "POST", body?: any) { + return fetch(this.getURL(path), { + method, + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + Authorization: `Password ${supernovaPassword}`, + }, + }); + } + + private alreadyReportedToSupernova( + error: Error + ): error is ErrorWithSupernovaID { + return "supernovaID" in error; + } } diff --git a/src/services/supernova/supernovaTypes.ts b/src/services/supernova/supernovaTypes.ts new file mode 100644 index 00000000..61bedc3e --- /dev/null +++ b/src/services/supernova/supernovaTypes.ts @@ -0,0 +1,20 @@ +export interface ErrorReportPayload { + message: string; + kind: string; + application: string; + userID: string; + severity: GowonErrorSeverity; + stack: string; + tags: ErrorReportTagPayload[]; +} + +export interface ErrorReportTagPayload { + key: string; + value: string; +} + +export enum GowonErrorSeverity { + WARNING = "warning", + ERROR = "error", + EXCEPTION = "exception", +}