Flare can now notify you via Discord and Microsoft Teams
When we launched Flare, we could notify you over errors occurring via Slack, Mail, SMS and Webhooks. When we take a look at our database, Slack seems to be the predominant way for teams to communicate.
In the meantime however, we got a lot of questions to add support for alternative notification platforms. Today, we're adding support for sending notifications via Discord and Microsoft Teams.
We took great care in formatting exception just right. If your teams uses Flare and Discord, this is how exceptions will look like in the Discord UI.
And here's how's it looks like when using Microsoft Teams.
In our documentation you can read how you can start using these new notification channels.
How these notification channels are implemented
Behind the scenes, Flare is a Laravel application. Out of the box, Laravel has easy to extend system for sending out notifications.
For Discord, we quickly implemented our own Discord notification channel and message.
This is the code of the DiscordChannel
class.
namespace App\Domain\Notification\Services\Channels\Discord;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Illuminate\Notifications\Notification;
class DiscordChannel
{
public function send($notifiable, Notification $notification): void
{
$discordMessage = $notification->toDiscord();
$discordWebhook = $notifiable->routeNotificationForDiscord();
(new Client())->post($discordWebhook, [
RequestOptions::JSON => $discordMessage->toArray(),
]);
}
}
And here's the code for the DiscordMessage
class. You'll notice that it's a bit opinionated. For convenience, we've added an errorOccurrence
method directly on the class that can format a given ErrorOccurrence
model.
namespace App\Domain\Notification\Services\Channels\Discord;
use App\Domain\Error\Models\ErrorOccurrence;
use App\Http\App\Controllers\Projects\ShowProjectController;
use Carbon\Carbon;
class DiscordMessage
{
public const COLOR_SUCCESS = '0b6623';
public const COLOR_WARNING = 'fD6a02';
public const COLOR_ERROR = 'e32929';
protected string $title = '';
protected string $description = '';
protected array $fields = [];
protected ?string $timestamp = null;
protected ?string $footer = null;
protected ?string $color = null;
protected string $url = '';
public function title($title)
{
$this->title = $title;
return $this;
}
public function url($url)
{
$this->url = $url;
return $this;
}
public function description($descriptionLines): self
{
if (! is_array($descriptionLines)) {
$descriptionLines = [$descriptionLines];
}
$this->description .= implode(PHP_EOL, $descriptionLines);
return $this;
}
public function timestamp(Carbon $carbon): self
{
$this->timestamp = $carbon->toIso8601String();
return $this;
}
public function footer(string $footer): self
{
$this->footer = $footer;
return $this;
}
public function success(): self
{
$this->color = static::COLOR_SUCCESS;
return $this;
}
public function warning(): self
{
$this->color = static::COLOR_WARNING;
return $this;
}
public function error(): self
{
$this->color = static::COLOR_ERROR;
return $this;
}
public function fields(array $fields, bool $inline = true): self
{
foreach ($fields as $label => $value) {
$this->fields[] = [
'name' => $label,
'value' => $value,
'inline' => $inline,
];
}
return $this;
}
public function errorOccurrence(ErrorOccurrence $errorOccurrence, string $message): self
{
$projectUrl = action(ShowProjectController::class, $errorOccurrence->error->project->idSlug());
$stage = $errorOccurrence->stage
? "({$errorOccurrence->stage})"
: "";
$this->title("{$errorOccurrence->error->project->name} {$stage}");
$this->url($projectUrl);
$fileLocation = "{$errorOccurrence->error->file}:`{$errorOccurrence->error->line_number}`";
if ($firstFrame = $errorOccurrence->firstFrame()) {
$fileLocation = "{$firstFrame->file}:`{$firstFrame->lineNumber}`";
}
$properties = [
"URL: {$errorOccurrence->seen_at_url}",
$message,
'',
"[See details on Flare]({$errorOccurrence->reportUrl()})",
"**{$errorOccurrence->exception_message}**",
'',
"**File:** {$fileLocation}",
'',
];
if ($firstFrame) {
$codeSnippet = $firstFrame->shortenCodeSnippetWithCurrentLineMark();
$occurredIn = "`{$firstFrame->class}::{$firstFrame->method}`" . PHP_EOL;
$this->fields([
$occurredIn => "```{$errorOccurrence->error->language}\n{$codeSnippet}```",
], inline: false);
}
$this->fields([
'Stage' => $errorOccurrence->stage,
'Exception' => $errorOccurrence->exception_class,
'Occurrence count' => $errorOccurrence->error->occurrence_count,
'Affected users' => $errorOccurrence->error->affected_user_count,
]);
$this->description($properties);
$this->timestamp($errorOccurrence->received_at);
return $this;
}
public function toArray(): array
{
return [
'username' => 'Flare',
'avatar_url' => 'https://flareapp.io/images/flare-avatar.png',
'embeds' => [
[
'title' => $this->title,
'url' => $this->url,
'type' => 'rich',
'description' => $this->description,
'fields' => $this->fields,
'color' => hexdec($this->color),
'footer' => [
'text' => $this->footer ?? '',
],
'timestamp' => $this->timestamp,
],
],
];
}
}
With this in place, we can add a toDiscord
method on our notification class.
namespace App\Domain\Error\Notifications\ErrorOccurred;
use App\Domain\Error\Models\ErrorOccurrence;
use App\Domain\Notification\Services\Channels\Discord\DiscordMessage;
class NewErrorOccurredNotification extends Notification
{
public function __construct(
public ErrorOccurrence $errorOccurrence
) {}
// other notifications channels omitted for brevity...
public function toDiscord()
{
return (new DiscordMessage())
->error()
->errorOccurrence(
$this->errorOccurrence,
'A new error occurred',
);
}
}
In closing
We're very happy with these new notification channels, and hope you'll like them as well. In the near future, you can expect more improvements around Flare's notification sytem.
If you want use to support your favourite notification channels, or have suggestions on how to make Flare better, let us know. We're listening!