Magento 2 Webhook Notifications, Discord & Slack

Webhook, M2, Discord & Slack

Webhook is an API concept that is becoming increasingly popular. More and more of what we do on the web can be described by events. Webhooks are an incredibly useful and easy way to conduct reactions to certain events of our choice. A specific example that we will cover in this post is “Sending notifications in the form of messages to the Slack channel and Discord server”.

What is a Webhook?

A webhook (also web callback or HTTP push API) is the way in which one application provides information to another application via callbacks. Webhooks are also called reverse APIs because they give you an API specification necessary for designing the API for the webhook. It mainly sends an HTTP request to the application, usually the POST method. This method is in charge of processing and interpretation. The disadvantage of labeling a webhook is supposedly a complex initial setup.

Create your own Incoming Webhook URL

For now, our modules will use Slack and Discord. However, there are many other applications that can be used as a notification system. Below is the basic information for creating an Incoming Webhook URL.

Discord – Server – Bot

Discord is a platform used by various types of communities in gaming as well as in education and entrepreneurship. It provides very simple and fast communication via voice, video, and text.

Creating a server on which we will use webhooks is quite simple. You just need to click on “Add a Server” and then select “Create a server“.

Adding a Discord Server
Creating a Discord Server

In the next view, you need to enter the server name and image (avatar).

Creating a Discord server name and image

After creating the server by default, there will be a text channel called “#general“. In this example, we will use that channel to display notifications from Magento 2. If you want notifications to be displayed on another channel on your Discord server, create that channel. Then, we can move on to the next step.

The next step is to create a webhook. In order to do that, you need to click on “Server Settings” from the drop-down menu in the upper left corner. In the next view, you need to select the “Webhooks” option.

Webhook - Discord Server Settings
Discord05

The last step is to create a Webhook. We do this as follows: press the “Create Webhook” button; a window with basic settings will open. Enter the name (bot name) in whose name the notifications (messages) will be displayed in the server channel. Then, select the server channel and finally Webhook Icon (Avatar). Webhook URL is very important because the Magento 2 Module will send data to the Discord server via that access point.

Editing Webhook

Eventually, it is necessary to save the Webhook. After that, the Discord server is ready to display the notification from Magento. As I’ve told you – nothing simpler.

Slack – Channel and App

For more information on how to set up the Slack channel and the application please follow the link.

Magento 2 Module

Let’s start with the implementation of a Magento 2 module. Its job will be to send exception information to Incoming Webhook URLs. The vendor of our module will be “SyncIt” while the module itself will be called “WebhookNotification“. If you want to make your module under your vendor name, feel free to do so. Our modules will contain the following files:

  • /composer.json
  • /registration.php
  • /etc/module.xml
  • /etc/di.xml
  • /etc/config.xml
  • /etc/adminhtml/system.xml
  • /Model/SystemConfig.php
  • /Model/Webhook.php
  • /Console/Command/Test.php

Creating a Module

Now, it is time to create a Magento 2 module.

composer.json

This file describes the dependencies of your project. Also, it contains other metadata that describe your package/module. At the following link, you can find descriptions on how to use all the tags that can be entered in the composer.json file. Here is an example of a well-created composer.json file:

{
    "name": "syncit/webhook-notification",
    "description": "SyncIt Webhook Notification module.",
    "version": "1.0.0",
    "type": "magento2-module",
    "keywords": [
        "syncit",
        "magento",
        "discord",
        "slack"
    ],
    "homepage": "https://www.syncitgroup.com",
    "time": "2020-07-14 18:01:33",
    "license": "GPL-3.0",
    "authors": [
        {
            "email": "[email protected]",
            "name": "Vladimir Đurović",
            "role": "Full Stack Developer"
        }
    ],
    "support": {
        "email": "[email protected]"
    },
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "SyncIt\\WebhookNotification\\": ""
        }
    },
    "minimum-stability": "dev"
}

registration.php

registration.php is a file used to register a model in the Magento system. Magento recognizes the module with its help and uses its functionality.

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'SyncIt_WebhookNotification',
    __DIR__
);

/etc/module.xml

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="SyncIt_WebhookNotification" setup_version="1.0.0"/>
</config>

/etc/adminhtml/system.xml

In system.xml we create an administrative configuration for our web pages. We need to configure the following fields:

  • The field that turns this functionality on and off
  • Permission to send Stack Trace values
  • Two fields with which we allow permission to send notifications (one for Discord and one for Slack)
  • Two fields in which we enter the Webhook URL for both Discord and Slack

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="syncit" sortOrder="4" translate="label">
            <label>SyncIt Group</label>
        </tab>
        <section id="webhook_notification" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10"
                 translate="label">
            <label>Webhook Notification</label>
            <tab>syncit</tab>
          <resource>SyncIt_WebhookNotification::config_syncit_webhooknotification</resource>
            <group id="general" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10"
                   translate="label">
                <label>General</label>
                <field id="enable" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10"
                       translate="label comment" type="select">
                    <label>Enabled</label>
                    <comment>Enable This Functionality</comment>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="enable_stack_trace" translate="label comment" type="select" sortOrder="20" showInDefault="1"
                       showInWebsite="0" showInStore="0">
                    <label>Stack Trace</label>
                    <comment>Enable Stack Trace</comment>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
            <group id="discord" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="20"
                   translate="label">
                <label>Discord</label>
                <field id="enable" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10"
                       translate="label comment" type="select">
                    <label>Enabled</label>
                    <comment>Enable Discord Webhook Notification</comment>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="webhook_url" translate="label comment" showInDefault="1" showInWebsite="0" showInStore="0"
                       sortOrder="20" type="text">
                    <label>Discord Webhook URL</label>
                    <comment>Discord Webhook URL</comment>
                </field>
            </group>
            <group id="slack" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="30"
                   translate="label">
                <label>Slack</label>
                <field id="enable" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="10"
                       translate="label comment" type="select">
                    <label>Enabled</label>
                    <comment>Enable Slack Webhook Notification</comment>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="webhook_url" translate="label comment" showInDefault="1" showInWebsite="0" showInStore="0"
                       sortOrder="20" type="text">
                    <label>Slack Webhook URL</label>
                    <comment>Slack App Incoming Webhook URL</comment>
                </field>
            </group>
        </section>
    </system>
</config>

/etc/config.xml

In this configuration file, we enter the initial values ​​of certain options. For example, we have set that when registering our module, the functionality of sending notifications is disabled, etc.


<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <webhook_notification>
            <general>
                <enable>0</enable>
                <enable_stack_trace>0</enable_stack_trace>
            </general>
            <slack>
                <enable>0</enable>
                <webhook_url>https://hooks.slack.com/services/XXXX/XXXX/XXXX</webhook_url>
            </slack>
            <discord>
                <enable>0</enable>
                <webhook_url>https://discordapp.com/api/webhooks/XXXX/XXXX</webhook_url>
            </discord>
        </webhook_notification>
    </default>
</config>

/Model/SystemConfig.php

The SystemConfig class is the class by which we obtain the value of the options we mentioned. There are also functions that format the text that will be displayed on the Discord or Slack.


declare(strict_types=1);

namespace SyncIt\WebhookNotification\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;

/**
 * Class SystemConfig | Get Store Config
 * @package SyncIt\WebhookNotification\Model
 */
class SystemConfig
{
    /**
     * Const for getting config from DB
     */
    const GENERAL_ENABLE = "webhook_notification/general/enable";
    const ENABLE_STACK_TRACE = "webhook_notification/general/enable_stack_trace";
    const GENERAL_STORE_INFORMATION_NAME = "general/store_information/name";

    /**
     * DISCORD
     */
    const DISCORD_ENABLE = "webhook_notification/discord/enable";
    const DISCORD_WEBHOOK_URL = "webhook_notification/discord/webhook_url";

    /**
     * SLACK
     */
    const SLACK_ENABLE = "webhook_notification/slack/enable";
    const SLACK_WEBHOOK_URL = "webhook_notification/slack/webhook_url";

    /**
     * @var ScopeConfigInterface
     */
    protected $_scopeConfig;

    /**
     * @var StoreManagerInterface
     */
    protected $_storeManager;

    /**
     * Data constructor.
     * @param ScopeConfigInterface $scopeConfig
     * @param StoreManagerInterface $storeManager
     */
    public function __construct(
        ScopeConfigInterface $scopeConfig,
        StoreManagerInterface $storeManager
    ) {
        $this->_storeManager = $storeManager;
        $this->_scopeConfig = $scopeConfig;
    }

    /**
     * Method for getting All config by name
     *
     * @param string $val => selector
     * @return mixed => Return value of filed in db
     */
    private function getValue($val)
    {
        return $this->_scopeConfig->getValue($val, ScopeInterface::SCOPE_STORE);
    }

    /**
     * Check if the extension is enabled
     *
     * @return bool => enable = true; disable = false;
     */
    public function isEnabled(): bool
    {
        return $this->getValue(self::GENERAL_ENABLE) ? true : false;
    }

    /**
     * Check if is enabled Stack Trace
     *
     * @return bool => enable = true; disable = false;
     */
    public function isEnabledStackTrace(): bool
    {
        return $this->getValue(self::ENABLE_STACK_TRACE) ? true : false;
    }

    /**
     * Check if is enabled Discord Webhook
     *
     * @return bool => enable = true; disable = false;
     */
    public function isEnabledDiscordWebHook(): bool
    {
        return $this->getValue(self::DISCORD_ENABLE) ? true : false;
    }

    /**
     * Get Discord Webhook URL
     *
     * @return string => URL
     */
    public function getDiscordWebHookURL(): ?string
    {
        return $this->getValue(self::DISCORD_WEBHOOK_URL);
    }

    /**
     * Check if is enabled Slack Webhook
     *
     * @return bool => enable = true; disable = false;
     */
    public function isEnabledSlackWebHook(): bool
    {
        return $this->getValue(self::SLACK_ENABLE) ? true : false;
    }

    /**
     * Get Slack Webhook URL
     *
     * @return string => URL
     */
    public function getSlackWebHookURL(): ?string
    {
        return $this->getValue(self::SLACK_WEBHOOK_URL);
    }

    /**
     * Get Store Name
     *
     * @return string
     */
    private function _getStoreName(): string
    {
        $val = $this->getValue(self::GENERAL_STORE_INFORMATION_NAME);
        if ($val) {
            return $val;
        }

        return 'Store Name';
    }

    /**
     * Get Message Subject for Slack
     *
     * @return string|null
     */
    public function getStoreInfoForSlack(): ?string
    {
        try {
            return '<' . $this->_storeManager->getStore()->getBaseUrl() . '|' . $this->_getStoreName() . '> | ' .
                'Notification: ';
        } catch (\Exception $exception) {
            return $this->_getStoreName() . ' | Notification: ';
        }
    }

    /**
     * Get Message Subject for Discord
     *
     * @return string|null
     */
    public function getStoreInfoForDiscord(): ?string
    {
        try {
            return $this->_getStoreName() . ' | <' . $this->_storeManager->getStore()->getBaseUrl() .
                '> | Notification: ';
        } catch (\Exception $exception) {
            return $this->_getStoreName() . ' | Notification: ';
        }
    }
}

/Model/Webhook.php

We get to the part where the magic happens. In the Webhook class, we notice the initial function (sendWebhook (string $ message, array $ stackTrace = [])). We will use it to start our functionality and make calls to Webhook through its URLs. To the sendWebhook function, we forward the message we want as well as Stack Trace in array type. It is the function that further checks whether sending notifications is enabled and whether sending Stack Trace is activated. If so, one or both functions are called to send notifications to the Discord server or Slack channel.

The _sendWebhookToDiscord and _sendWebhookToSlack functions prepare and send HTTP requests via the POST method to the URLs (Webhook URLs). They also prepare a request body that is populated with the desired information, in our case, exception information. Finally, we notice another function called _parseStackTrace. Its task is to format the Stack Trace array into a string value with the “code” tags (` ` `). In this way, the display of the Stack Trace will be more visible in the message itself.


declare(strict_types=1);

namespace SyncIt\WebhookNotification\Model;

use SyncIt\WebhookNotification\Model\SystemConfig as WebhookSystemConfig;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\HTTP\Adapter\CurlFactory;
use Psr\Log\LoggerInterface;

/**
 * Class Webhook | Send message
 * @package SyncIt\WebhookNotification\Model
 */
class Webhook
{
    /**
     * @var SystemConfig
     */
    protected $_webhookSystemConfig;

    /**
     * @var CurlFactory
     */
    protected $_curlFactory;

    /**
     * @var LoggerInterface
     */
    protected $_logger;

    /**
     * Webhook constructor.
     * @param SystemConfig $webhookSystemConfig
     * @param CurlFactory $curlFactory
     * @param LoggerInterface $logger
     */
    public function __construct(
        WebhookSystemConfig $webhookSystemConfig,
        CurlFactory $curlFactory,
        LoggerInterface $logger
    ) {
        $this->_webhookSystemConfig = $webhookSystemConfig;
        $this->_curlFactory = $curlFactory;
        $this->_logger = $logger;
    }

    /**
     * Send Webhook with message and stack trace
     *
     * @param string $message
     * @param array $stackTrace
     */
    public function sendWebhook(string $message, array $stackTrace = []): void
    {
        if (!$this->_webhookSystemConfig->isEnabled()) {
            return;
        }

        if ($this->_webhookSystemConfig->isEnabledSlackWebHook()) {
            $this->_sendWebhookToSlack($message, $stackTrace);
        }

        if ($this->_webhookSystemConfig->isEnabledDiscordWebHook()) {
            $this->_sendWebhookToDiscord($message, $stackTrace);
        }
    }

    /**
     * Method for make call to Slack
     *
     * @param string $message => Text Message
     * @param $stackTrace = > Stack Trace
     */
    private function _sendWebhookToSlack(string $message, $stackTrace)
    {
        $text = $this->_webhookSystemConfig->getStoreInfoForSlack() . $message;

        if ($this->_webhookSystemConfig->isEnabledStackTrace() && $stackTrace !== []) {
            $text .= $this->_parseStackTrace($stackTrace);
        }

        $slackUrl = $this->_webhookSystemConfig->getSlackWebHookURL();

        $client = $this->_curlFactory->create();
        $client->addOption(CURLOPT_CONNECTTIMEOUT, 2);
        $client->addOption(CURLOPT_TIMEOUT, 3);
        $client->write(
            \Zend_Http_Client::POST,
            $slackUrl,
            '1.1',
            ['Content-type: application/json'],
            json_encode(["text" =>  $text])
        );

        $response = $client->read();

        if (\Zend_Http_Response::extractCode($response) !== 200) {
            $this->_logger->log(100, 'Failed to send a message to the following Webhook: ' . $slackUrl);
        }

        $client->close();
    }

    /**
     * Method for make call to Discord
     *
     * @param string $message => Text Message
     * @param $stackTrace = > Stack Trace
     */
    private function _sendWebhookToDiscord(string$message, $stackTrace)
    {
        $text = $this->_webhookSystemConfig->getStoreInfoForDiscord() . $message;

        if ($this->_webhookSystemConfig->isEnabledStackTrace() && $stackTrace !== []) {
            $text .= $this->_parseStackTrace($stackTrace);
        }

        $discordUrl = $this->_webhookSystemConfig->getDiscordWebHookURL();

        $client = $this->_curlFactory->create();
        $client->addOption(CURLOPT_CONNECTTIMEOUT, 2);
        $client->addOption(CURLOPT_TIMEOUT, 3);
        $client->write(
            \Zend_Http_Client::POST,
            $discordUrl,
            '1.1',
            ['Content-type: application/json'],
            json_encode(["content" =>  $text])
        );

        $response = $client->read();

        if (\Zend_Http_Response::extractCode($response) !== 200) {
            $this->_logger->log(100, 'Failed to send a message to the following Webhook: ' . $discordUrl);
        }

        $client->close();
    }

    /**
     * Convert Stack Trace Array to String for command
     *
     * @param array $stackTrace => Stack Trace array
     * @return string => Stack Trace string
     */
    private function _parseStackTrace(array $stackTrace = []): string
    {
        $trace = [];
        foreach ($stackTrace as $row => $data) {
            if (!array_key_exists('file', $data) || !array_key_exists('line', $data)) {
                $trace[] = "# <unknown>";
            } else {
                $trace[] = "#{$row} {$data['file']}:{$data['line']} -> {$data['function']}()";
            }
        }
        return PHP_EOL . '```' .  implode("\n", $trace) . '```';
    }
}

/etc/di.xml

We will register our command that will help us test the operation of our functionalities, i.e., functions.

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\Console\CommandList">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="Test" xsi:type="object">SyncIt\WebhookNotification\Console\Command\Test</item>
            </argument>
        </arguments>
    </type>
</config>

/Console/Command/Test.php

At the very end of creating a Magento 2 module, in my opinion, the best practice is to create a test command if it is necessary to create a command to run a certain functionality. So, in this example, we have decided on the most famous mathematical exception “Division by Zero“. We have created a simple try-catch segment where we will try to divide a number by zero in the try block. And in the catch block, we will call our function which will pass the exception information to the Webhook URL.


declare(strict_types=1);

namespace SyncIt\WebhookNotification\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use SyncIt\WebhookNotification\Model\Webhook;

/**
 * Class Test | extends Command
 * @package SyncIt\WebhookNotification\Console\Command
 */
class Test extends Command
{
    /**
     * @var Webhook
     */
    protected $_webhook;

    /**
     * Test constructor.
     * @param Webhook $webhook
     */
    public function __construct(
        Webhook $webhook
    ) {
        $this->_webhook = $webhook;
        parent::__construct('syncit:webhook:test');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(
        InputInterface $input,
        OutputInterface $output
    ) {
        try {
            $divisionByZero = 5 / 0;
        } catch (\Exception $exception) {
            $this->_webhook->sendWebhook(
                $exception->getMessage(),
                $exception->getTrace()
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName("syncit:webhook:test");
        $this->setDescription("Command for testing Webhook Notification");
        parent::configure();
    }
}

Module Setup

After creating and registering the module, it is necessary to enable the options from the Admin Panel. In addition, you need to enter Webhook URLs for Discord and Slack depending on what you want to use. After setting up the module, you can proceed to test the module’s functionality.

Magento 2 Admin Panel, Store Configuration

Module Testing – (Functionalities)

We perform functionality testing via the created command:

php bin/magento syncit:webhook:test

Immediately after running the command you will receive a notification on Discord or Slack with information about the exception that occurred on your Magento 2 site. The messages you receive will look like this.

Discord

Discord Notifications

Slack

Slack Notifications

Wrap Up

And that’s it. Just follow the steps that we have thoroughly explained and you will successfully create Webhooks in no time.

Sources

TechTerms

Discord Help Center

Slack API

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments