Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use webhooks for discord notification #84

Merged
merged 13 commits into from
Nov 6, 2024

Conversation

jahabeebs
Copy link
Collaborator

@jahabeebs jahabeebs commented Nov 4, 2024

🤖 Linear

Closes GRT-238

Description

note: for the avatarURL in discordNotifier we are using an image hosted by a third party--can we deploy this icon somewhere instead so we don't have to rely on the alternative site? we could have a simple static site for Wonderland assets maybe?

note 2: in the unit tests for the discord notifier I give the operator the option of enabling useRealDiscordWebhook in which they can replace the mock with their own. Do we want to keep this there or better with just the mock? It would be nice to give the operator a chance to do the integration test but I don't want to add a .env to this package (which is not an app) just so we can add a VITE_DISCORD_WEBHOOK variable

integration test output:

Screenshot 2024-11-04 at 6 15 54 PM

Copy link

linear bot commented Nov 4, 2024

@jahabeebs jahabeebs changed the title fix: use webhooks for Discord notification fix: use webhooks for discord notification Nov 4, 2024
Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff over here! Left some comments related to design

@@ -1,8 +1,7 @@
import { z } from "zod";

const ConfigSchema = z.object({
DISCORD_BOT_TOKEN: z.string().min(1),
DISCORD_CHANNEL_ID: z.string().min(1),
DISCORD_WEBHOOK: z.string().min(1),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could using .url() result in a little bit more precise schema? [1]

[1] https://zod.dev/?id=strings

Suggested change
DISCORD_WEBHOOK: z.string().min(1),
DISCORD_WEBHOOK: z.string().url()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason I didn't do this was because of integration testing (not sure if people ever test w/local webhooks?) but since we're not worrying about it for now I replaced this with url().optional() (I verified it is indeed optional)

Comment on lines 8 to 9
// Change this to `true` to test your Discord webhook URL set below rather than mocking it
const useRealDiscordWebhook = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice that you have thought of a way to actually send messages, getting closer to an integration test! And it's also nice that you got to see the DiscordNotifier in action.

Using the actual Discord webhook will probably help us during the E2E tests, although it is kinda complex to test stuff for this particular service, given that we should actually check in the proper Discord channel that the message was sent + it was nicely formatted + the content is ok + etc.

Let's keep this as a plain unit tests for the moment, fully mocking discord.js and later we can talk about testing Discord also E2E.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored to be plain unit tests

} else {
await notifier.send(message);

const WebhookClientMock = WebhookClient as unknown as vi.Mock;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be that if you remove the if when mocking, you won't need this WebhookClientMock declaration and casting anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 indeed

it("sends an error message to the Discord channel", async () => {
const error = new Error("Test error");
const context = { key: "value" };
it("sends a message successfully", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the describe("send", ...) block wrapping these tests

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch ✅

Comment on lines 35 to 39
try {
await client.login(config.discordBotToken);

await new Promise<void>((resolve, reject) => {
client.once("ready", () => {
logger.info("Discord bot is ready");

resolve();
});

client.once("error", (error: Error) => {
reject(error);
});
});
await this.client.send(webhookMessage);
console.error(this.client, this.client);
} catch (error) {
logger.error(`Failed to initialize Discord notifier: ${error}`);

throw error;
throw new NotificationFailureException("An error occurred with the Discord client.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we want the agent to keep working even if there's an error during Discord notifications, we've got two options right now:

  1. Not throwing anything and just logging if an error happens during Discord message sending actions
  2. Keep this throw here, but handle every error with a try-catch block or similar

The option (1) seems like the safest as, at the moment, the agent has no place where a notification failure is the cause of the whole agent to fail. The option (2) seems like the more generalized option though, as it's throwing the error and letting the client handle it.

I suggest we do the following:

  • Let's keep this send method, but let's make it log the error if there's an error
  • Let's add a new sendOrThrow method that does what this method is doing at the moment

Wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separated the logic out into the send and sendOrThrow logic as you suggested 👍🏻

return {
username: message.username || "EBO Agent",
content: message.title,
avatarURL: message.avatarUrl || "https://cryptologos.cc/logos/the-graph-grt-logo.png",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the URL to be a new config param (new value in the config yml called notifier.discordDefaultAvatarUrl which is a string().url() + optional() if Discord allows you to specify no avatar URL) to be set during the class constructor:

Suggested change
avatarURL: message.avatarUrl || "https://cryptologos.cc/logos/the-graph-grt-logo.png",
avatarURL: message.avatarUrl || this.defaultAvatarUrl,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ it was indeed optional so I added that

*/
notifyError(error: Error, context: any): Promise<void>;
createErrorMessage(err: unknown, context: unknown, defaultMessage: string): IMessage;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like context might be empty (or {}), but the defaultMessage is always being used. Do you think we should probably swap the order of those two parameters and make the context's default value to be {} if not specified?

Also, I've seen that whenever you define the message inside the context that you are passing to this method, you are using the same value as the one used passed as defaultMessage. Maybe you can remove the message declaration from the context in those function calls?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. good point, I swapped the order and have a default {} value for context if not specified now
  2. indeed for some of these the context is redundant--in eboProcessor I removed the places where we were passing in the same error message as the context

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could add some link referring to Discord docs on how to create your Webhook

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});
});
await this.client.send(webhookMessage);
console.error(this.client, this.client);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.error(this.client, this.client);

i guess this log shouldn't be here right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right 😅

if (err instanceof Error) {
this.onActorError(requestId, err);
} else {
this.onActorError(requestId, new Error(String(err)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does String(err) do the job? isn't it possible that it renders [object Object]? how is it different from JSON.stringify?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally needed this because of a type thing but I've removed this now

Comment on lines 341 to 351
const errorMessage = this.notifier.createErrorMessage(
new Error("Unsupported chain"),
{
message: `Chain ${chainId} not supported by the agent. Skipping...`,
chainId,
requestId,
},
`Chain ${chainId} not supported by the agent. Skipping...`,
);

this.notifier.send(errorMessage);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although this was the original way I implemented this, i'm thinking if one should create the error message and then send it or if creating the errorMessage should be internal to the implementation (this would mean modifying the Interface to not receive an IMessage but rather the signature from createErrorMessage)

cc @0xyaco @0xkenj1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done some refactoring of discordNotifier.ts to hopefully make things more concise--wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me, nice job 🙌

@jahabeebs jahabeebs requested review from 0xnigir1 and 0xyaco November 6, 2024 01:19
*/
constructor(url: string) {
constructor(url: string, defaultAvatarUrl?: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's inject logger. And added some minor tweaks, just being consistent with the rest of our codebase:

Suggested change
constructor(url: string, defaultAvatarUrl?: string) {
constructor(
url: string,
private readonly logger: ILogger,
private readonly defaultAvatarUrl?: string
) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chainId,
requestId,
},
this.notifier.sendError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we await these sendError calls? I'm afraid that if something unexpectedly raises an error within the promise, it might crash the whole agent.

Copy link
Collaborator Author

@jahabeebs jahabeebs Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure we can, I was just trying to avoid having to make a bunch of the eboProcessor functions async but I went ahead and changed them

);

this.notifier.send(errorMessage);
this.notifier.sendError(`Actor error for request ${requestId}`, { requestId }, error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

`Could not create a request for epoch ${epoch} and chain ${chain}.`,
);

this.notifier.sendError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

);

await this.notifier.send(errorMessage);
this.notifier.sendError("Error creating missing requests", { epoch }, err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

message: `Actor handling request ${requestId} was already terminated.`,
requestId,
},
this.notifier.sendError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@jahabeebs jahabeebs requested a review from 0xyaco November 6, 2024 17:29
Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet!

Copy link
Collaborator

@0xnigir1 0xnigir1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go go

@jahabeebs jahabeebs merged commit 2c02271 into dev Nov 6, 2024
5 checks passed
@jahabeebs jahabeebs deleted the fix/grt-238-discord-webhook branch November 6, 2024 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants