-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
# Conflicts: # packages/automated-dispute/src/exceptions/index.ts
There was a problem hiding this 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), |
There was a problem hiding this comment.
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
DISCORD_WEBHOOK: z.string().min(1), | |
DISCORD_WEBHOOK: z.string().url() |
There was a problem hiding this comment.
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)
// Change this to `true` to test your Discord webhook URL set below rather than mocking it | ||
const useRealDiscordWebhook = false; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch ✅
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."); |
There was a problem hiding this comment.
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:
- Not throwing anything and just logging if an error happens during Discord message sending actions
- Keep this
throw
here, but handle every error with atry-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?
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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:
avatarURL: message.avatarUrl || "https://cryptologos.cc/logos/the-graph-grt-logo.png", | |
avatarURL: message.avatarUrl || this.defaultAvatarUrl, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- good point, I swapped the order and have a default {} value for context if not specified now
- 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
apps/agent/README.md
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.error(this.client, this.client); |
i guess this log shouldn't be here right?
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
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); |
There was a problem hiding this comment.
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
)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🙌
*/ | ||
constructor(url: string) { | ||
constructor(url: string, defaultAvatarUrl?: string) { |
There was a problem hiding this comment.
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:
constructor(url: string, defaultAvatarUrl?: string) { | |
constructor( | |
url: string, | |
private readonly logger: ILogger, | |
private readonly defaultAvatarUrl?: string | |
) { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
go go
🤖 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: