Build a custom Twilio alerting system using Express.js, json-rules-engine, and MongoDB

Vikhyath SVikhyath S
19 min read

Introduction

Ahoy builders! In the world of technology, fixing errors quickly is key. Alerting is crucial because it helps us stay proactive rather than reactive. When something goes wrong in our systems or applications, it is often time-sensitive. Alerts notify us when there is an issue, allowing us to take swift action to mitigate any potential damage or downtime.

Twilio’s Console Debugger allows developers to configure an optional webhook to receive data about errors and warnings as they happen. This makes it straightforward for developers to react to any problems with their applications, promptly. While Twilio’s Console Debugger excels in error detection, its notification capabilities are further enhanced by Twilio Alarms, extending the alerting functionality to email notifications. You can set alerts for any warning or error, or for specific errors while defining a threshold value.

While Twilio Alarms offer robust notification capabilities, they may not address every specialized requirement. In some cases, you may find the need to create a custom alerting system tailored to specific requirements. For instance, you might want to set an alert for a group of error codes or for a particular Twilio resource such as a Twilio Function or a Studio flow. Additionally, you might require a single alert to monitor multiple accounts, including subaccounts.

In this tutorial, you will be building a fully customizable alerting system using Express.js, json-rules-engine, MongoDB, the Twilio Debug Events Webhook, and Twilio SendGrid to send email.

Prerequisites

To proceed with this tutorial, you will need the following:

Set up your development environment

Create the project

Before building the application, you need to set up your development environment. Open up your terminal and execute the following commands:

mkdir express-alert
cd express-alert
npm init -y

This command creates an express-alert directory and navigates into it. The npm init command will initialize a project and create a package.json file. The -y flag will choose the default settings while creating the project.

Install the dependencies

You will be using the following packages in your application.

  • express - Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

  • twilio - A package that allows you to interact with the Twilio API.

  • dotenv - A zero-dependency module that loads environment variables from a .env file into process.env .

  • json-rules-engine - A powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.

  • @sendgrid/mail - This library allows you to quickly and easily use the Twilio SendGrid Web API v3 via Node.js.

  • mongodb - The official MongoDB driver for Node.js.

Install all the packages using the following command:

npm install express twilio dotenv json-rules-engine @sendgrid/mail mongodb

Once the packages are installed, you will see an output similar to the following:

Screenshot displaying npm install command output: Indicates added packages, audited packages, packages seeking funding, and total vulnerabilities detected.

Set up environment variables

You can use environment variables to store sensitive data required later in your application. Create an .env file in your project directory and paste the snippet given below:

TWILIO_MAIN_ACCOUNT_SID=ACXXXXXXXXX

You can find your Main Account SID from Twilio Console.

Configure SendGrid

You will be using Twilio SendGrid to send alerts via email. Follow the steps below to get started with Twilio SendGrid. Sign up for SendGrid and create an API key

The first thing you need to do is create a SendGrid account. You can choose the free tier for this tutorial. After setting up your account, generate an API key with “Full Access”, as shown in the screenshot. You're free to choose any name for it, but ensure you save it before proceeding!

Image depicting the process of generating an API Key in SendGrid. It includes a mandatory 'API Key Name' field for entering the key's name, three vertically aligned radio buttons for selecting permissions ('Full Access', 'Restricted Access', 'Billing Access'), and buttons for creating ('Create & View') or canceling the process.

Now, update your .env file with the SendGrid API Key as shown below:

SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxxx

Verify your Sender Identity

To ensure the sender’s reputation, Twilio SendGrid requires you to verify the identity of your “From” email address by completing domain authentication.

You can verify one or more Sender Identities using either Domain Authentication or Single Sender Verification. You can go to the Sender Authentication page in the SendGrid Console and click on Verify a Single Sender. Once complete, add the necessary details (you can refer to this post if you have any questions) and click on Create.

Once created, you will receive an email to the sender address. Make sure to open the link contained in the email and verify the sender.

To learn more about Sender Verification, you can refer to this documentation.

After verifying the sender, update your environment variables to include both From and To email addresses as shown below:

TO_EMAIL=xyz@gmail.com
FROM_EMAIL=abc@gmail.com

Create a SendGrid dynamic template

In this tutorial, you will be using a SendGrid dynamic template to customize your alerts.

Open the Dynamic Templates page and click on the Create a Dynamic Template button. After creating the template, add a Version to it. Select Code Editor to design your email. Copy and paste the html content given below.

For detailed instructions, you can refer to our blog post on Dynamic Email Templates.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Twilio Alert Template</title>
  <style>
    body {
      background-color: #f2f2f2;
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
    }
    .container {
      background-color: #ffffff;
      border: 2px solid #cccccc;
      border-radius: 10px;
      margin: 50px auto;
      padding: 20px;
      width: 80%;
    }

    table {
      border-collapse: collapse;
      width: 100%;
    }
    th {
      background-color: #f2f2f2;
      border: 1px solid #cccccc;
      padding: 8px;
      text-align: center;
    }
    td {
      border: 1px solid #cccccc;
      padding: 8px;
      text-align: center;
    }
    h3 {
      color: red;
      display: block;
      text-align: center;
    }
  </style>
</head>
<body>

<div class="container">
  <h3>Twilio Alert</h3>

  <table>
    <tbody>
      <tr>
        <td>Account SID</td>
        <td>{{body.AccountSid}}</td>
      </tr>
      <tr>
        <td>Timestamp</td>
        <td>{{body.Timestamp}}</td>
      </tr>
      <tr>
        <td>Error Code</td>
        <td>{{body.Payload.error_code}}</td>
      </tr>
      <tr>
        <td>Service SID</td>
        <td>{{body.Payload.service_sid}}</td>
      </tr>
      <tr>
        <td>Resource SID</td>
        <td>{{body.Payload.resource_sid}}</td>
      </tr>
    </tbody>
  </table>

  <br>

  <h4 style="text-decoration: underline; color: #000000;">Webhook Payload</h4>
  <p style="white-space: pre-line; color: #000000;">{{body.Payload.webhook.response.body}}</p>


  <a style=" background-color:#0056b3;
      color:#ffffff;
      border-radius: 5px;
      display: block;
      margin: 20px auto;
      padding: 10px;
      text-align: center;
      text-decoration: none;
      width: 150px;
      border:1px solid #333333;
      border-width:1px;" href="https://console.twilio.com/us1/monitor/logs/debugger/errors/{{body.Sid}}">More Details</a>
</div>

</body>
</html>

After pasting the code, assign a Version Name to your template. Utilize {{{subject}}} in the Subject field to dynamically include the subject. Then, click on the Save button to proceed.

A screenshot illustrating the process of saving dynamic templates in SendGrid. The image is split into two sections, with a prominent top bar featuring various buttons including a back button (depicted by a left arrow) and a Save button, both highlighted by red arrows. On the left side, the 'dynamic version settings' interface is displayed, showcasing fields such as 'Version name' and 'Subject,' with the latter highlighted in a red rectangular box.

After saving the template, navigate to the previous page by clicking on the back button (the left facing arrow) as shown above, and copy the Template ID

A screenshot of the SendGrid console displaying the dynamic templates page. One template named 'Twilio Alert' is visible, with a 'Template ID' highlighted in a red rectangular box.

Update your environment variable with the Template ID as shown below:

SENDGRID_TEMPLATE_ID=d-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Configure MongoDB

You will be using MongoDB Atlas to store the alert count and timestamps. MongoDB Atlas is a fully-managed database service offered by MongoDB, providing the capability to store data in key-value pairs. Additionally, it offers a free plan featuring limited storage. Given our use case of storing only two fields per record (alert count and timestamps), MongoDB Atlas is an ideal solution.

Follow the steps below to get started with MongoDB Atlas.

Sign up for MongoDB Atlas and create a Cluster

Create a MongoDB Atlas account using one of the available sign in options. When you first create an account in MongoDB Atlas, you will see the following screen.

Image illustrating the process of creating a MongoDB cluster. The M0 cluster option and the 'Create' button are highlighted within a red rectangular box.

For this tutorial, we will opt for the free Shared cluster, but feel free to adjust its configuration to suit your requirements. Click on Create to continue.

Once the Cluster is created, you will be asked to complete the security quickstart. In this tutorial, you will be using username and password authentication. So, provide a username and password, as shown below, and click on Create User.

Image displaying the 'Security Quickstart' interface in MongoDB. The image is divided into two sections: a left sidebar and a configuration section. Within the configuration section, the option for 'username and password' authentication is selected. Additionally, there are fields for entering a username and password.  A 'Create User' button is highlighted within a red rectangular box.

After creating the user, you need to add your IP address to the IP Access List. Atlas only allows client connections to a cluster from entries in the project’s IP Access List. Simply select Add My Current IP Address to append your IP to the list. Once all IPs are included, proceed to Finish and Close.

Continuing from the Security Quickstart interface in MongoDB, the section now focuses on enabling access to networks. The 'My Local Environment' option is selected. Within this section, the 'Add My Current IP Address' button and the 'Finish and Close' button are highlighted within red rectangular boxes.

Create a Database and Collection

Now that your cluster is prepared, let’s set up a database to store our collection.

Click on the Add Data option as depicted below to proceed. You will then be directed to a page where you can create a new database and collection.

Image displaying the database deployment section in MongoDB. Various options such as 'Add Data', 'Load Sample Data', and 'Data Modelling Templates' are visible. The 'Add Data' option is highlighted within a red rectangular box.

You will be creating the Database on Atlas, so click on START as shown below.

Image displaying the 'Add Data' options in MongoDB. Within the 'Create Database on Atlas' option, a red arrow points to the 'Start' button.

You can choose any name for the database and collection. For example, you might use twilio-alerts for the database and threshold for the collection name. Finally, to complete the setup, click on Create Database.

Image showing the 'Create Database on Atlas' page in MongoDB. The 'Database name' and 'Collection name' fields are highlighted within red rectangular boxes. Additionally, the 'Create Database' button at the bottom is also marked in red.

Update the database and collection details in the .env file

MONGO_DB_DATABASE=twilio-alerts
MONGO_DB_COLLECTION=threshold

Copy the connection details and update .env file

Click on the CONNECT button on the overview page to get connection details (as shown below).

Image displaying the MongoDB overview section. A red arrow points to the 'Connect' button.

Copy the connection string and paste it in the .env file as shown below.

Image displaying MongoDB connection details. The connection string is highlighted within a red rectangular box.

MONGO_DB_URI=mongodb+srv://vikhyath:<password>@cluster0.a8cmdpw.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0

Replace <password> with your actual password.

Build your Application

Create an Express Server

You will be using Express.js to build this application. An Express server is a web application framework for Node.js, designed to build web applications and APIs. It simplifies the process of creating robust and scalable server-side applications by providing a set of powerful features such as routing, middleware, and other features you will need in a server.

Let’s set up a basic express server. We will use it to handle webhooks from Twilio, which include debugging information we will use to form our alerts.

In your project directory (express-alert), create a file called app.js and paste the code snippet given below:

import express from 'express'
const app = express()
const port = 3000
app.use(express.urlencoded({ extended: true }));


app.get('/', (req, res) => {
res.send('Hello World!!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

Here you are using ES Modules, so make sure to add "type": "module" in your package.json file. You can refer to this document if you want to learn more.

As you can see from the first line, you import express using the ‘import’ keyword and an instance of the Express application is created by calling the express() function. Finally, you call the listen() method on your Express application, passing in the port number and a callback function that will be executed once the server starts.

The Twilio webhook passes the parameters in url-encoded format, so you need to use the express.urlencoded({ extended: true }) middleware.

The express.urlencoded() function is a built-in middleware function in Express. It parses incoming requests with URL-encoded payloads and is based on a body parser. This middleware is available in Express v4.16.0 onwards.

Create the rule engine

Here you will be creating a Rule Engine. You will use json-rule-engine to filter the incoming webhook payload and send an email alert only for specific events.

json-rules-engine is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.

Create a file called ruleEngine.js and paste the code code snippet given below.

import { Engine } from 'json-rules-engine';
import 'dotenv/config'

const FUNCTION_ERROR_CODES_BEGIN = "82001"
const FUNCTION_ERROR_CODES_END = "82009"

const STUDIO_ERROR_CODES_BEGIN = "81000"
const STUDIO_ERROR_CODES_END = "81026"

const mainAccountSid = process.env.TWILIO_MAIN_ACCOUNT_SID;
const subAccounts = [ "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", “ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX” ] //Here you can list all your subaccounts
const FUNCTION_SID = "ZHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

const rulejson1 = {  
    conditions: {
      any: [{
        all: [
          {
          fact: 'webhookData',
          operator: 'in',
          value: subAccounts,
          path: '$.AccountSid'
        }, 
        {
          all: [
              {
              fact: 'webhookData',
              operator: 'greaterThanInclusive',
              value: FUNCTION_ERROR_CODES_BEGIN,
              path: '$.Payload.error_code'
            },
            {
              fact: 'webhookData',
              operator: 'lessThanInclusive',
              value: FUNCTION_ERROR_CODES_END,
              path: '$.Payload.error_code'
            },
            {
                fact: 'webhookData',
                operator: 'equal',
                value: 'ERROR',
                path: '$.Level'
            }
            ]
        }]
    },
  ]
    },
    event: { 
      type: 'subAccountError',
      params: {
        message: 'Error in Twilio Function',
        action: 'send-email',
        data: {
            fact: 'webhookData',
        }
      }
    }
}

const rulejson2 = {
  conditions: {
    any: [{
      all: [
        {
        fact: 'webhookData',
        operator: 'equal',
        value: mainAccountSid,
        path: '$.AccountSid'
      }, 
      {
        all: [
            {
            fact: 'webhookData',
            operator: 'equal',
            value: FUNCTION_SID,
            path: '$.Payload.service_sid'
          },
          {
              fact: 'webhookData',
              operator: 'equal',
              value: 'ERROR',
              path: '$.Level'
            }
          ]
      }]
  }
]
  },
  event: { 
    type: 'functionError',
    params: {
      message: `Error in Function ${functionSid}`,
      action: 'send-email',
      data: {
          fact: 'webhookData',
      }
    }
  }
}

const rulejson3 = {
  conditions: {
    any: [{
      all: [
        {
        fact: 'webhookData',
        operator: 'equal',
        value: mainAccountSid,
        path: '$.AccountSid'
      }, 
      {
        all: [
          {
          fact: 'webhookData',
          operator: 'greaterThanInclusive',
          value: STUDIO_ERROR_CODES_BEGIN,
          path: '$.Payload.error_code'
        },
        {
          fact: 'webhookData',
          operator: 'lessThanInclusive',
          value: STUDIO_ERROR_CODES_END,
          path: '$.Payload.error_code'
        },
        {
            fact: 'webhookData',
            operator: 'equal',
            value: 'ERROR',
            path: '$.Level'
        }
        ]
      }]
  }
]
  },
  event: { 
    type: 'studioError',
    params: {
      message: 'Error in Studio flow',
      action: 'send-email',
      eventType: "condition",
      data: {
          fact: 'webhookData',
      }
    }
  }
}

const thresholdRule = {
  [rulejson1.event.type]: {
    count: 3,
    threshold: 60000
  },
  [rulejson2.event.type]: {
    count: 5,
    threshold: 60000
  },
  [rulejson3.event.type]: {
    count: 10,
    threshold: 60000
  }
}
const engine = new Engine([], { replaceFactsInEventParams: true })
engine.addRule(rulejson1)
engine.addRule(rulejson2)
engine.addRule(rulejson3)
export { engine, thresholdRule };

In the above snippet, you'll develop a Rule Engine to assess three rules: detecting errors thrown by any function across your sub accounts, identifying errors thrown by a specific function SID in the main account, and recognizing errors thrown by any Studio flow in your main account.

To implement this, you need to import Engine from json-rules-engine. Twilio passes data to your webhook in the form of application/x-www-form-urlencoded. Within that request body, the Payload property is a JSON object that you will need to decode. You can find the full list of parameters here.

The rulejson1 contains a rule to match errors thrown by any function across all your sub accounts.

The rulejson2 contains a rule to match errors thrown by a specific function SID in the main account.

The rulejson3 contains a rule to match errors thrown by any studio flow in your main account.

The variables STUDIO_ERROR_CODES_BEGIN and STUDIO_ERROR_CODES_END contain the beginning and ending values of Studio related errors. Similarly, FUNCTION_ERROR_CODES_BEGIN and FUNCTION_ERROR_CODES_END contain the beginning and ending values of Functions related errors. You can find the full list of error codes here.

The variable thresholdRule sets a threshold for each rule to ensure the triggering of alarms. The threshold values are set in milliseconds.

Once the JSON rules are created, you need to create an engine and pass replaceFactsInEventParams: true to include the webhook data in the event parameters. Add all the three rules to the Engine object using the addRule method. Now, export the engine and thresholdRule using export { engine, thresholdRule } to use it in other files.

Create a route for incoming webhook

Now you will create an endpoint in your express application to handle incoming webhook requests. Paste the following code snippet in your app.js file:

import { alertMiddleware } from './alertMiddleware.js'

// Your endpoint with middleware
app.post('/alert', alertMiddleware, (req, res) => {
    res.send('event received');
});

Here the alertMiddleware is the middleware function you will be creating shortly, and it is used to parse incoming webhook data and apply the logic to send email alerts.

Implement the middleware logic

Now, you will be implementing the middleware to handle the webhook requests. Create a new file called alertMiddleware.js in the root directory and paste the code snippet given below.

import { engine, thresholdRule } from './ruleEngine.js';
import 'dotenv/config'
import { MongoClient } from 'mongodb'
import { sendEmailAlert } from './sendEmail.js'


const mongodbDatabase = process.env.MONGO_DB_DATABASE
const mongodbCollection = process.env.MONGO_DB_COLLECTION
const uri = process.env.MONGO_DB_URI


const mongoClient = new MongoClient(uri)
await mongoClient.connect();


const db = mongoClient.db(mongodbDatabase);
const collection = db.collection(mongodbCollection);


const alertMiddleware = async (req, res, next) => {
    const mainJSON = req.body;
    mainJSON.Payload = JSON.parse(req.body.Payload);
    await validateRules(mainJSON);
    next();
};


async function validateRules(data) {
    const docArray = await collection.find({}).toArray();
    engine.addFact('webhookData', data);
    const { events } = await engine.run();


    for (const event of events) {
        const message = event.params.data;
        const eventType = event.type;
        var item = docArray.find(item => item[eventType]);
        var diff = item ? new Date(message.Timestamp).getTime() - item[eventType].lastUpdatedTime : 0;
        if (!item) {
            item = {
                [eventType]: {
                    count: 1,
                    lastUpdatedTime: new Date(message.Timestamp).getTime()
                }
            };
            docArray.push(item);
            await collection.insertOne(item);
        } else if (diff > thresholdRule[eventType].threshold) {
            item[eventType].count = 1;
            diff = thresholdRule[eventType].threshold;
            item[eventType].lastUpdatedTime = new Date(message.Timestamp).getTime();
            await updateItem(item, eventType);
        } else {
            item[eventType].count++;
            item[eventType].lastUpdatedTime = new Date(message.Timestamp).getTime();
            await updateItem(item, eventType);
        }
        console.log(item[eventType])
        if (item[eventType].count === thresholdRule[eventType].count && diff <= thresholdRule[eventType].threshold) {
            console.log("Threshold Reached!");
            sendEmailAlert(event.params.message, message);
        }
    }
}
async function updateItem(item, eventType) {
    const filter = { _id: item._id };
    const updateDoc = {
        $set: {
            [`${eventType}.count`]: item[eventType].count,
            [`${eventType}.lastUpdatedTime`]: item[eventType].lastUpdatedTime
        }
    };
    await collection.updateOne(filter, updateDoc);
}
export { alertMiddleware };

The code above defines an alertMiddleware function, which serves as middleware for processing incoming webhook data. This function extracts the JSON payload from the incoming request, parses it, and then calls the validateRules function to evaluate the defined rules.

The validateRules function will first retrieve all the threshold data from the MongoDB collection. Then it adds the webhook data as a fact to the rule engine. Then it runs the rule engine and iterates over the events triggered by the rules. For each event, it retrieves the relevant data from the MongoDB collection array docArray based on the event type. Then, it calculates the time difference between the current event and the last occurrence of the event in the database. Depending on the calculated time difference and the configured threshold for the event type, it updates the database and potentially triggers an email alert if the threshold condition is met.

Implement the SendGrid logic

Now that we have completed the middleware, it is time to implement the sendEmailAlert function we have used in the middleware. Create a file sendEmail.js and paste code snippet given below:

import sgMail from '@sendgrid/mail';
import 'dotenv/config'
sgMail.setApiKey(process.env.SENDGRID_API_KEY);


function sendEmailAlert(subject, body){
    const msg = {
        templateId: process.env.SENDGRID_TEMPLATE_ID,
        to: process.env.TO_EMAIL,
        from: process.env.FROM_EMAIL,
        subject: subject.toString(),
        dynamicTemplateData: {
            "subject": subject,
            "body": body       
        }
    };
    sgMail
    .send(msg)
    .then(() => {}, error => {
        console.error(error);
        if (error.response) {
        console.error(error.response.body)
        }
    });
}
export { sendEmailAlert }

Run your Express Server

Let's kick off your server so you can handle the webhook requests. Navigate to your project directory (twilio-alerts) and run this command.

node app.js

It will start an express server at port 3000 and you can access the server using the URL http://localhost:3000. You should see a “Hello World!” message when you open this URL in the browser.

The Express server can't be reached online because it's local to your machine. Twilio needs to send web requests to this server, so during development, you'll have to make the local server available to the Internet. To do this, we’ll use ngrok.

Open a second terminal window and run the following command:

ngrok http 3000

The ngrok screen should look as follows:

Terminal output displaying the result of the command 'ngrok http 3000'. The output includes the ngrok public URL, which can be utilized to set the debugger webhook URL.

When ngrok is active, you can reach the application from anywhere globally using the temporary forwarding URL displayed in the command output. Any web requests sent to the ngrok URL will be directed to the Express application by ngrok.

Configuring the Twilio webhook

Once you've set up the webhook, the next step is to configure the debugger webhook URL in the Twilio console. This ensures that you receive webhook requests whenever an alert is logged. Navigate to the Twilio Console and paste the ngrok forwarding URL with https in it into Webhook URL (as shown below).

Image depicting the debugger webhook configuration section in the Twilio console. The layout is divided into two parts: a left sidebar with various options and the right side for making configurations. It features a 'Webhook URL' field for providing the debugger webhook URL. The checkbox option labeled 'Include subaccount errors and warnings' is selected.

Don't forget to check the box for Include sub account errors and warnings to also capture errors and warnings from sub accounts.

Test your Application

It’s time to test your application. Your express server will send an email notification whenever the error logs match the rules we have set using the rule engine.

To test your application, you can create a simple Studio flow with a Make HTTP Request Widget which tries to make a request to an invalid URL (for example, you can set the URL to https://urlnotfound/). When this flow is executed, it should throw an error, and you should receive an email alert as shown below

Note that the webhook payload displayed in the email depends on the error thrown since the webhook property of the payload is optional. You can always customize the look and feel of your email for your use case using dynamic templates. The More Details button will have a link to your error log in the debugger console. Conclusion By using Twilio Debug event webhooks, a JSON-rule engine, MongoDB, Express.js, and SendGrid you've just created a customizable alerting system over email. This system ensures peace of mind, as you will get quick notifications if something important goes wrong in your application.

What's great is that this solution can be changed in a straightforward manner. Right now, it uses the Debug Event Webhook, but you can add other webhook sources too. This means you can make the system better and add rules based on different webhook formats.

With this strong alerting system, you can stay on top of things and make sure your app runs smoothly, fixing problems before they become big issues.

0
Subscribe to my newsletter

Read articles from Vikhyath S directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vikhyath S
Vikhyath S