Build a Custom Error Reporting System with Twilio SendGrid and Django

February 13, 2025
Written by
Oghenevwede Emeni
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Building a Custom Error Reporting System with Twilio SendGrid and Django

Introduction

One of the many things I know — as a backend engineer who has worked in many early-stage startups and built a few — is it can be really expensive to set up 3rd-party error reporting systems within your code, especially when your team has a tight budget to work with. Most times, as developers, we need to set up systems in our code to help us track errors and report them efficiently. This helps us respond quickly to issues and keeps the application running smoothly.

In this article, you will learn how to set up a custom error reporting system using Twilio SendGrid and Django. This system will help track and report errors in your code quickly and as soon as they are found, so that the right teams in your company can take action right away.

Prerequisites

To complete the tutorial, you will need the following:

  • A free SendGrid account
  • Python 3.x installed on your machine. If you don’t already have it installed, you can go to python.org to download an installer.
  • Basic knowledge of Django and how to set up a Django project
  • A text editor like VS Code or PyCharm
  • An understanding of how environment variables work.

Setting up the Django project

Creating the project folder and setting up a Python Virtual environment

To get started, you need to create a folder that will house your Django project. So, in your terminal, navigate to the directory where you want to create this folder, then run the commands below:

mkdir error_reporting_project
cd error_reporting_project

Creating a Python Virtual environment

Now that your folder has been created, you need to create a virtual environment. This helps keep your project dependencies isolated, making sure that whatever you're working on doesn't interfere with other Python projects you have on your system. To create a virtual environment, run the following command:

python3 -m venv env

If you are on Windows, run this right after to activate your environment.

env\Scripts\activate

On macOs or other Linux based systems, run the command below to activate:

source env/bin/activate

When this is done, you will notice (env) at the start of your terminal prompt. This means your virtual environment is active and working. It should look like this:

environment example

Installing Django

Next, you need to install Django within this virtual environment. Django is a Python high level web framework that makes it easy to build secure web applications. It comes with a lot of built in tools that help with things like database management, user authentication, and URL routing.

pip install django

To verify the installation was successful, run the command below to check your version

django-admin --version

You should see the Django version number, which means that Django was installed successfully.

Starting a Django project

Next, you need to create a Django project. This project sets up all the necessary structure and configuration you need for your application. It creates important files and directories, such as settings for database connections, static files, and templates, and provides a solid foundation for building and managing your app efficiently.

django-admin startproject error_reporting

Configure Twilio SendGrid

You will need a SendGrid API key, which you can get easily from your SendGrid dashboard. To proceed, log into your SendGrid dashboard, and on the left side menu bar, click on the API Keys option. On that page, click on the Create an API Key button.

You will be prompted to create a name for this key. Give your API key a meaningful name, such as “Django Reporting App.” For simplicity in this tutorial, select Full Access so you can perform any action within your account.

After clicking the create button, you will be shown your SendGrid API key. Make sure to copy it immediately and store it in a secure file, in this case, the .env file. Replace yoursendgridapikey with your actual key. Be sure to save this key for the next step. If you lose the key, you will need to create a new one, because it is only shown once.

You also need to install some important packages like python-dotenv to load your environment variable into the project and sendgrid to communicate with Twilio SendGrid servers. You can also save your installed packages to your requirements.txt file for version control.

pip install sendgrid python-dotenv
pip freeze > requirements.txt

Configuring environment variables

After your project has been set up successfully, the next step is to configure your environment variables. To do this, create a .env file in the root of your project. This file holds sensitive information, such as your SendGrid API key, and keeps them secure and out of version control. In your .env file, add the following:

SENDGRID_API_KEY= yoursendgridapikey

Make sure to include .env in your .gitignore file so that sensitive details are not tracked in version control. Simply create a file in the root of your folder and name it . gitignore. Then add your .env file, like this:

.env

Set Up Logging

Logging is great for helping to track and trace issues easily, analyse performance, and improve the overall reliability of your application. This way you can monitor important events, such as failed requests or unexpected behaviour.

In your error_reporting/settings.py, you can make use of Django’s inbuilt logging to log errors and important information. While there isn’t a specific required spot for this code, it’s generally advisable to add it towards the end of the settings.py file, after all other settings have been defined, to maintain a clean and organized structure.

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': 'errors.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

The code above will make sure that any errors in your Django application are logged to a file called errors.log.

Set Up Your Config File

Next, you need to set up which teams need to be notified about errors. This could be anything from your engineering team, your entire team, or just the QA team. Here, you can list as many teams as necessary and simply match them to the correct email address. In your project root, create the config.json file where the email addresses of your teams will be stored. For example:

{
    "team_emails": {
        "dev_team": "devteam@gmail.com",
        "ops_team": "opsteam@gmail.com",
        "qa_team": "qateam@gmail.com"
    }
}
Don't forget to add the config.json file to your .gitignore to prevent it from being accidentally committed to version control. It's also beneficial to put your virtual environment in this file by adding the line /env.

Create and Register a New Django App

Within your project you need to set up a Django app specifically for error reporting. A Django project can have one or more apps, each of which will have their own code for routes, models, and views. In this application you will use a single-app architecture. To create the app, run the command in your terminal below:

cd error_reporting
python manage.py startapp reports

This will create a reports folder within your product directory. To register this app and make Django aware of your new app, you need to add it to the INSTALLED_APPS list in the error_reporting/settings.py file. This way you are telling Django to load the configuration and routes defined in the reports app. Open the file, seek out the following code block, and add the highlighted line to the block of installed apps.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'reports', 
]

Creating the error reporting API

Create the views

Views work like a restaurant waiter who waits to take your order (request), processes it by telling the kitchen (business logic), and then serves the dish (response) to you (user). They handle the logic for processing requests, interacting with models, and returning responses.

The goal here is to set up a Django view that listens for incoming HTTP POST requests. It will receive error details, such as the error type, message and severity. Then it will process it, log it and then send it to the team that needs to be notified using the email service.

Set up the basic structure of your view

In this tutorial you will be making use of the function-based view (FBV). It uses a more straightforward approach to handling requests and responses than Classes. To get started you will need to import the necessary modules such as;

  • json: for parsing JSON data from the incoming request.
  • logging: to log errors for debugging purposes.
  • os: For loading environment variables like your Sendgrid API key.
  • SendGrid API: To send emails to teams using the Twilio SendGrid service.
  • dotenv: To load environment variables from a .env file.
  • csrf_exempt: A decorator to exempt the view from CSRF protection because you are expecting external services (like your frontend) to send data.

Add the following imports to your reports/views.py file where you plan to handle the email functionality.

import json
import logging
import os
from django.shortcuts import render
from django.http import JsonResponse
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from dotenv import load_dotenv
from django.views.decorators.csrf import csrf_exempt

Next, you need to load your SendGrid API key.

load_dotenv() 
SENDGRID_API_KEY = os.getenv('SENDGRID_API_KEY')

Then you set up logging to log every error report that is received. This way you have a record of all errors.

logger = logging.getLogger(__name__)

Now you will need to load the email addresses of the teams who should receive the error notifications. All you need to do is to call the config.json file, so that it can be read in your code. Using config.json makes it so much easier for your team to add or change email addresses without needing to modify the code.

# Load team email configurations
def load_team_emails():
    with open('./config.json') as f:
        config = json.load(f)
    return config["team_emails"]

Send an email with the error details

The next step is for you to send an email containing the error details to the right team. For this, use the SendGrid API, which makes it easy to send emails swiftly with only a few lines of code.

# Send error report to the selected team
def send_email(to_email, subject, content):
    message = Mail(
        from_email='youremail@email.com”',  # Change this to your verified SendGrid sender email
        to_emails=to_email,  # Recipient email
        subject=subject,  # Subject of the email
        html_content=content)  # The error details in HTML format (easy readability)
    try:
        sg = SendGridAPIClient(SENDGRID_API_KEY)
        response = sg.send(message)  
        return response 
    except Exception as e:
        logger.error(f"Failed to send email: {e}") 
        return None

Here is what happens when your application tries to send a mail:

  • This function creates a SendGrid Mail object with the provided email details. It contains who is sending, who is receiving, the subject as well as what message they need to know about.
  • Then it uses the SendGrid API to send the email and returns the response. If any error occurs during the email-sending process, it catches the exception, and logs an error message.

Defining your error reporting view

This is the view that handles the POST requests. It will parse the incoming request to extract the error details and then send an email with those details to the right team.

# Report error view
@csrf_exempt
def report_error(request):
    if request.method == 'POST':
        # Get the payload from the request
        payload = json.loads(request.body)
        # Extract details from the payload
        error_type = payload.get("error_type")
        message = payload.get("message")
        application = payload.get("application")
        severity = payload.get("severity")
        timestamp = payload.get("timestamp")
        context = payload.get("context")
        notify_teams = payload.get("notify", [])
        # Log the error
        logger.error(f"Error Type: {error_type}, Message: {message}, Severity: {severity}, Timestamp: {timestamp}, Context: {context}")
        # Load team emails
        team_emails = load_team_emails()
        # Send emails to selected teams
        for team in notify_teams:
            if team in team_emails:
                email = team_emails[team]
                subject = f"Error Report: {error_type} - {severity}"
                content = f"<h3>Application: {application}</h3><p><b>Error:</b> {message}</p><p><b>Severity:</b> {severity}</p><p><b>Timestamp:</b> {timestamp}</p>"
                response = send_email(email, subject, content)
                if response:
                    return JsonResponse({"message": "Error report sent successfully."}, status=200)
                else:
                    return JsonResponse({"message": "Failed to send error report."}, status=500)
        return JsonResponse({"message": "No valid teams to notify."}, status=400)
    return JsonResponse({"message": "Invalid request method."}, status=400)

So here is what is happening in your error report view:

  • @csrf_exempt: is a decorator that is trying to exempt the view from Django’s CSRF protection, and it's necessary because the request comes from external sources.
  • request.method == 'POST': this makes sure the view only handles POST requests.
  • json.loads(request.body) : parses the JSON body of the request into a Python dictionary.
  • payload.get() : extracts specific pieces of information from the parsed payload e.g the error type, message, and severity

Logging is also implemented to help you debug and monitor. After that, you will notice the email loops through the teams specified in the notify_teams list, in the request body and sends an email to each team using the send_email function. You get a success message if the email sends successfully or the appropriate error response, if something goes wrong.

Setting up the app URLs

For your report_error view to be accessible through a specific URL or endpoint, you need to configure Django’s URL routing. This involves you, defining URLs in the app’s urls.py and then including the app's URLs in the main project’s urls.py file.

First, define the URL in the reports app. To do this you need to head over to the reports folder and create a file called reports/urls.py. Add the following code.

from . import views
urlpatterns = [
    path("errors", views.report_error, name="report_error"),
]

In this path("errors", views.report_error, name="report_error") the URL errors is mapped to the report_error view. This means when your user visits (yourdomain)/errors, Django will trigger the report_error function from views.py so that it can handle the request.

Next you need to include the reports app URLs in the main project’s URL configuration. Look for urls.py in the error_reporting folder and add the report_error path to the list of URL patterns. Be sure to import the views as well. The full code for the file will now read:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('report_error/', views.report_error, name='report_error'),
]

This path('report_error/', views.report_error, name='report_error') line, includes all URL patterns from the reports/urls.py file and makes them available at the /report_error/ path.

Test your work

Save your work and make sure everything has been put together properly. Next, test your app. To test this app, run the Django development server:

python manage.py runserver

Note: If you're seeing a warning about unapplied Django migrations, you can apply them by running:

python manage.py migrate

Your terminal should look like this -

Performing system checks...
System check identified no issues (0 silenced).
January 30, 2025 - 16:09:09
Django version 5.1.5, using settings 'error_reporter.settings'        
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

Once the server is running, open a browser or a tool like Postman and navigate to this URL (or whatever you have named yours) - http://127.0.0.1:8000/report_error/ to access the error reporting endpoint. Then send a sample POST request like the one below, to verify the app captures and processes the information correctly. You should see a confirmation message or status, letting you know if it was successful or not.

{
  "error_type": "DeveloperError",
  "message": "Null pointer exception",
  "application": "MyApp",
  "severity": "Critical",
  "timestamp": "2025-01-27T12:00:00Z",
  "context": {"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"},
  "notify": ["dev_team"]
}

Your success message should look like this -

Success message showing a successful error report sent.

Troubleshooting Tips

Here are some tips to guide you, in case you run into any issues.

  • If your app is not loading or URL not found, double check that the reports app is included in INSTALLED_APPS in settings.py.
  • If you are using Twilio's Sendgrid trial account, you might be restricted to sending 100 emails daily.

Conclusion

Congratulations on setting up a complete error reporting system in Django! You have learnt how to create views, send customised emails through Twilio SendGrid, and configure Django to handle error notifications for your team.

If you would like to see this project in a pre-built form, please view it on GitHub here.

Remember email sending is really monitored, so try not to spam people or send the wrong thing. As a follow up you should consider checking out the Twilio Sendgrid's documentation and blog to learn more. Learn about features like IP warming and sender reputation management to improve the chances of your mails landing in your user' inbox and not getting blocked. To learn more about Django views and error handling, check out the Django documentation on views and logging. Thanks for reading!

Oghenevwede is a Senior Product Engineer with 8 years of obsessing about how things work on her phone and on the web. Her new obsession is health and wellness.