Share on Twitter
Share on Facebook
Share on HackerNews

Monitoring Performance and Errors in a Django Application with Sentry

Sentry is a monitoring platform that allows developers to track errors and performance data.

In this tutorial, we’ll show you how to add Sentry to a Django application so that you can track and resolve any errors or performance issues that occur while your application is in production.

Prerequisites

You will need the following in order to follow along with the tutorial:

  • An account with Sentry
  • Python 3.4+ development environment
  • pgAdmin4 and PostgreSQL

Getting Started

Create a Sentry account by navigating to the signup page and filling in your details or signing up with your Google or GitHub account.

02-signup

When you’ve successfully created your account, you will be redirected to the “Welcome” page which is shown in the image below.

03-welcome

Click on the “Start” button, after which you will be taken to the “Select Platform” page. Select Django and click on the “Create Project” button.

04-frameworks

Next, you will be taken to a page with instructions on how to prepare the Sentry Django SDK for it to work with your application.

05-django-instructions

Project Set Up

In this step we’ll prepare the development environment we’ll be working in and install the required packages.

Create a Virtual Environment

A virtual environment allows us to isolate packages for different applications. We’ll create a virtual environment for our Django application so we can define only the packages needed for it to run.

Run the command below in a terminal while in your preferred working directory to create the virtual environment:

python3 -m venv djangoenv

To activate the environment, run one of the following commands depending on your OS.

MacOS/Linux

source djangoenv/bin/activate

Windows

.\djangoenv\Scripts\activate

Install Packages

We can now add packages to our virtual environment.

Run the commands below to install the packages we need:

pip3 install django djangorestframework
pip3 install --upgrade sentry-sdk

Create a Django Project

A Django project can be thought of as a container for different applications that will be working together within it in order to bring about the project’s overall desired functionality. Create a boilerplate Django project by running the command:

django-admin startproject sentrydjango

Create and Set Up an Application

In this step we’ll extend the starter project by adding an application to it so as to give it meaningful functionality.

Navigate into the project’s root folder by running cd sentrydjango. From here, we can initialize our application by running:

python3 manage.py startapp names

This command creates a names directory that will house our application’s src code.

Now we’ll add the names application to our project by adding it to the INSTALLED_APPS list in the sentrydjango/settings.py file, like so:

INSTALLED_APPS = (
   'names.apps.NamesConfig',
   'rest_framework',
    ...,
    ...,
)

Take care to add the “names.apps.NamesConfig” entry as the first item in the list. We also need to register the djangorestframework package we installed earlier using the string, rest_framework.

Register Application Models and Serializers

Open the models.py file in the “names” directory and add the code below to define a Person model:

from django.db import models

class Person(models.Model):
   name = models.CharField(max_length=200)

We’re giving the model only a name field to keep things simple and ensure the tutorial is easy to follow.

Now we’ll add a Person serializer to handle validating and translating data from JSON into a format that can be saved in the database and vice versa when we perform read operations. Create a names/serializers.py file and add the code below to it:

from rest_framework import serializers
from .models import Person

class PersonSerializer(serializers.ModelSerializer):
   name = serializers.CharField(max_length=200)

   class Meta:
       model = Person
       fields = ('__all__') # all fields required

Create Application Views

Let’s add views to perform the different HTTP methods on our dataset. Open the names/views.py file and add the following code to it to define routes for posting and getting data from the database:

from django.shortcuts import render
from django.http import request
from rest_framework.views import APIView
from .serializers import PersonSerializer
from .models import Person

def index(request):
   return render(request, "index.html")

class PersonAPIViews(APIView):
   def post(self, request):
       serializer = PersonSerializer(data=request.data)
       #check if data is valid then save else return error
       if serializer.is_valid():
           serializer.save()
           return render(request, "index.html", context={"message": "Save Successful"})
       else:
           return render(request, "index.html", context={"message": "Save Not Successful"})

   def get(self, request, id=None):
       #fetch all persons
       persons = Person.objects.all()
       serializer = PersonSerializer(persons, many=True)
       return render(request, "names-list.html", context={"persons": serializer.data}
)

You’ll notice the serializer we defined earlier is used to validate POST data before we save it. When it’s a GET request, the serializer is used to convert database models into JSON data that can be easily sent to the frontend.

We use class-based views by inheriting from Django REST framework’s APIView class. In the event of a POST request the post method is called and for a GET request the get method is called. When we’re done modifying the backend we’ll look at how to add the frontend templates referenced in the above views.

Add Application URLs

We can now add the application URLs. Create a urls.py file inside the names directory and populate it with the code below:

from django.urls import path
from .views import PersonAPIViews
from .import views

urlpatterns = [
   path('', views.index, name='index'),
   path('names', PersonAPIViews.as_view()),
]

Let’s add our application’s URLs to the Django project. Open sentrydjango/urls.py and replace its content with the following code:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
   path('admin/', admin.site.urls),
   path('app/', include('names.urls')),
]

The “names” application index route (def index(request) in views.py) will be available at localhost:8000/app/, since we added the application’s routes under the app/ route.

Add PostgreSQL Database

Our application’s backend is now only short of a database. Let’s add a PostgreSQL database to enable persistent storage. Open pgAdmin4 and create a new database called “dbtest”.

Next, open the sentrydjango/settings.py file and modify the DATABASES section with the code below:

DATABASES = {
   'default': {
       'ENGINE': 'django.db.backends.postgresql_psycopg2',
       'NAME': 'dbtest',
       'USER': 'postgres',
       'PASSWORD': '<YOUR VALUE HERE>',
       'HOST': '127.0.0.1',
       'PORT': '5432',
   }
}

Take care to replace the PASSWORD value with your own.

Run Database Migrations

Now we can tell our database which tables our application needs in order to work by running the relevant migrations. Migrations translate the models defined in our application into their respective tables in the database.

While in the project’s root folder, run the command:

python3 manage.py makemigrations names

This command will generate the SQL statements that need to be run against the database in order to create the tables for the “names” application.

Next, run the command:

python3 manage.py migrate

This command executes the SQL statements generated by the previous command.

Add Application Frontend

With the application’s backend now complete, we can focus on the frontend. We’ll need two templates, one for adding names and one for reading a list of all submitted names. Create a names/templates directory to house our application’s frontend templates. Inside the templates directory, create a file named index.html and populate it with the code below:

<!DOCTYPE html>
<html lang="en">
<head>
   <title>Names App</title>
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
       integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
   <style>
       body{
           padding: 20px;
       }
   </style>
</head>

<body>
   <h1>Submit Your Name</h1>
   {% if message %}
       <h3>{{ message }}</h3>
   {% endif %}

   <form action="/app/names" method="post" class="mb-3">
       <input type="text" placeholder="Name" name="name" class="form-control mb-3" />
       <button type="submit" class="btn btn-primary">Submit</button>
   </form>
   <a href="/app/names">View Submitted Names</a>
</body>
</html>

Here, we are creating a form which handles the submission of new names by sending the POST data to the /names route. The <head> tag includes a link to Bootstrap to make use of Bootstrap’s styling library.

Now create a names-list.html file inside the templates folder. The purpose of this file will be to display the list of names that have been submitted by our application’s users. Add the following code to the names-list.html file:

<!DOCTYPE html>
<html lang="en">
<head>
   <title>Names App</title>
   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
       integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
   <style>
       body{
           padding: 20px;
       }
   </style>
</head>

<body>
   <h1>Submitted Names</h1>
   <ul>
   {%for person in persons%}
       <li>{{ person.name }}</li>
   {%endfor%}
   </ul>
    <a href="/app/">Go To Home</a>
</body>
</html>

The for loop on the page loops through the list of names that have been supplied by the GET request.

Add Sentry to Django Application

Let’s add Sentry to our Django application to see it in action. Open the sentrydjango/settings.py file and add the code below to initialize the Sentry Django SDK on project startup:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
   dsn="<YOUR VALUE HERE>",
   integrations=[DjangoIntegration()],

   # Set traces_sample_rate to 1.0 to capture 100%
   # of transactions for performance monitoring.
   # We recommend adjusting this value in production,
   traces_sample_rate=1.0,

   # If you wish to associate users to errors (assuming you are using
   # django.contrib.auth) you may enable sending PII data.
   send_default_pii=True,

   # By default the SDK will try to use the SENTRY_RELEASE
   # environment variable, or infer a git commit
   # SHA as release, however you may want to set
   # something more human-readable.
   # release="myapp@1.0.0",
)

Take care to replace the dsn value with your own. Find your dsn value on Sentry’s “Prepare the Django SDK” page still open in your browser.

18-dsn

Add faulty code

In order to test Sentry, we need to add code that’ll trigger an error when run in production. Over the next few steps, we’ll add code with different kinds of errors to see if Sentry is able to pick up each of them.

The first error we’ll introduce to our codebase is a wrong database password. We’ll then try to access our database with the wrong password and see if Sentry picks it up. Open the sentrydjango/settings.py file and scroll to the database configuration section and replace your password with any other string.

For the second test, we’ll add a zero division error to one of our application’s views. Open the sentrydjango/urls.py file and modify its contents so that it looks like the snippet below:

from django.contrib import admin
from django.urls import path, include

def trigger_error(request):
   division_by_zero = 1 / 0

urlpatterns = [
   path('admin/', admin.site.urls),
   path('app/', include('names.urls')),
   path('sentry-debug/', trigger_error),
]

Visiting the /sentry-debug route will call the trigger_error method, which will in turn trigger the zero division error.

The last error we’ll test for is an “undefined” error. Open the names/views.py file and modify the get method to reference an undefined variable like so:

def get(self, request, id=None):
       #fetch all persons
       error = undefined_var
       persons = Person.objects.all()
       serializer = PersonSerializer(persons, many=True)
       return render(request, "names-list.html", context={"persons": serializer.data})

Test Sentry Integration

Before we test whether we added everything correctly, go to Sentry’s platform on the “Prepare the Django SDK” page and scroll to the bottom. Take note of the text at the bottom notifying you that Sentry is still waiting to detect an error from your code since you haven’t triggered any yet.

06-wait-for-error

Test Case 1

Go to your terminal and start the Django application by running the command: python3 manage.py runserver. You should get an authentication error since you’re trying to access the database with wrong credentials. Head back to your Sentry console on the “Prepare the Django SDK” page and you should find that the text at the bottom has changed to “Event was received!“.

This confirms that we added Sentry correctly and that it’s working as expected.

Resolve Test Case 1

While on the “Prepare the Django SDK” page on Sentry, click on the “Take me to my error” button on the lower left of the page. This will take you to your “Issues” tab where you can find out more details about the error that occurred.

08-operational-error

From the image above, we can see that Sentry was able to pick up the operational error and what it’s about, since the description says, “Access denied for user …“. Let’s fix the error by changing the password back to its true value and then restarting the application. This time the application should start successfully.

If the restart went as expected, we can mark the error as resolved on Sentry’s “Issues” tab by clicking on the “Resolve” tag on the upper-left section of the page.

Test Case 2

To demonstrate how Sentry handles zero division errors, visit the http://localhost:8000/sentry-debug route in your browser. There you should see the following error:

09-zero-error

Resolve Test Case 2

Return to your Sentry console and take a look at the error in your “Issues” tab. You’ll notice the issue name is displayed in blue.

10-unresolved-errors

Click on the error name to get a detailed view of the error:

11-error-summary

Let’s resolve the error by modifying the trigger_error method in urls.py to look like so:

from django.http import HttpResponse

def trigger_error(request):
   try:
       division_by_zero = 1 / 0
   except:
       division_by_zero = "Hello World"

   return HttpResponse(division_by_zero)

Save your changes and visit the http://localhost:8000/sentry-debug route in the browser. You should get a “Hello World” message. We can now mark the error as resolved on Sentry’s “Issues” tab.

Test Case 3

Navigate to the http://localhost:8000/app/names/ route of our application which will call the get method in our Person view. You should see the following error in your browser notifying you that there’s an undefined variable:

13-name-error

Resolve Test Case 3

Head over to the “Issues” tab on your Sentry console to check if the error was picked up.

14-name-error-summary

Fix the error by removing the code that references the undefined variable in the get method. Visit the http://localhost:8000/app/names route again, and it should display successfully now.

You can resolve the error on Sentry.

Performance monitoring

Inconsistent application experiences aren’t only due to errors or crashes. In addition to error monitoring, we need to monitor our application for slow responses and long page loads. Sentry’s performance monitoring capabilities allow you to keep your application’s performance healthy and optimized.

Let’s take a look at how to add performance moniotoring to our Django application, and then we’ll analyze the reports in Sentry as before.

To enable performance monitoring, we only need to install Sentry’s SDK and configure our traces sample rate. Both of these we have already done.

Navigate to your settings.py file and adjust your traces_sample_rate to 1.0 if it isn’t already:

   traces_sample_rate=1.0,

This tells Sentry to capture 100% of the applications transactions for performance monitoring.

A Little Terminology

In Sentry, transactions are defined as calls that occur between our application’s sevices or microservices. Transactions are composed of spans, which are the function calls and code executions within the application’s services. Multiple transactions are seen as traces. Traces are a record of the entire operation, page load, or action being measured.

In the next two test cases, we’ll modify code at the span level and see how the rest of the transaction and trace is affected. For the sake of brevity, we’ll focus on resolving Test Case 5, but the analysis and remedy is similar for Test Case 4.

Test Case 4

Let’s simulate a slow page load, possibly due to a large resource.

Go to the urls.py in the sentrydjango folder and add the following function:

def large_resource(request):
   time.sleep(4)
   return HttpResponse("Done!")

This function will serve as a proxy for loading a large resource. Python’s time.sleep() function suspends execution for a given number of seconds.

We need to import the time module, so add this code at the top of the file:

import time

Now add the function onto the application’s routes. Your code should look like this:

urlpatterns = [
   path('admin/', admin.site.urls),
   path('app/', include('names.urls')),
   path('sentry-debug/', trigger_error),
   path('large_resource/', large_resource)
]

Visit http://localhost:8000/large_resource and it should display “Done!” after some time.

Test Case 5

For our final test case, we’ll simulate poor performing code by adding another time delay to the POST method.

Navigate to views.py in the names directory, and add the delay line to the POST function:

def post(self, request):
       serializer = PersonSerializer(data=request.data)
       # Poor performance code
       time.sleep(4)
       #check if data is valid then save else return error
       if serializer.is_valid():
           serializer.save()
           return render(request, "index.html", context={"message": "Save Successful"})
       else:
           return render(request, "index.html", context={"message": "Save Not Successful"})

Import the time module at the top of the file.

Visit http://localhost:8000/app and submit a name to trigger the post action.

You should notice the programmed delay in both the page load and action.

In the case that our delays aren’t programmed, we need to figure out exactly where the slow down is and how it’s affecting our users.

Let’s take look at the Sentry dashboard to verify and analyze this.

Resolve Performance Errors

Navigate to the “Performance” tab on Sentry and you’ll be greeted by a few graphs and tables. We’ll keep our focus narrowed on the transactions table for this example.

15-performance

Sort the table using the P95 threshold column. This is the explanation from the Sentry docs:

The P95 Threshold indicates that 5% of transaction durations are greater than the threshold. For example, if the P95
threshold is 50 milliseconds, then 5% of transactions exceeded that threshold, taking longer than 50 milliseconds.

Transaction thresholds can be modified, but the current threshold is fine for our example.

The first row of the table has our POST request (yours might be a little different). Clicking on “/app/names” takes us to a summary view of the transaction with more breakdowns, maps, scores, and any related issues linked to the transaction.

16-transaction-summary (with arrow)

From the supplied information, we can see that the transaction’s total duration consists of individual spans, each with their own durations.

As expected, the time delay we’ve added is hurting our application’s performance, evident in the amount of time the transaction took to complete. This example demonstrates Sentry’s ability to debug your application’s performance when it is in your users hands.

Click on “names.views.PersonAPIViews” under “Suspect Spans” and you’ll be given another view detailing the span’s duration.

17-span-summary

This specific span takes up 68% of our transaction due to the time delay we added to the POST method. The remainder of the duration is taken up by the database connection span, which may also be a point of required optimization.

We can fix the performance issue by removing the time delay in the code. If we trigger the POST action again, the span duration is reduced, and our application is back to being as peformant as it could be.

We hope you find Sentry useful and if you do, you’ll be glad to know it supports other popular tech stacks as well in addition to Django.

Your code is broken. Let's Fix it.
Get Started

More from the Sentry blog

ChangelogDashboardsDiscoverDogfooding ChroniclesEcosystemError MonitoringEventsGuest PostsOpen SourceRelease HealthSDK UpdatesSentry

Do you like corporate newsletters?

Neither do we. Sign up anyway.

© 2022 • Sentry is a registered Trademark
of Functional Software, Inc.