Nik Tomazic
Posted on March 13, 2024
In this post, we will specifically look at using AppSignal to track errors in a Django application.
We'll first create a Django project, install AppSignal, introduce some faulty code, and then use the AppSignal Errors dashboard to debug and resolve errors.
Let's get started!
Prerequisites
To follow along, you'll need:
- Python 3.8+ installed on your local machine
- An AppSignal-supported operating system
- An AppSignal account
- Basic Django knowledge
Project Setup
We'll be working on a movie review web app. The app will allow us to manage movies and reviews via a RESTful API. To build it, we'll utilize Django.
I recommend you follow along with the movie review web app first. After you grasp the basic concepts, using AppSignal with your projects will be easy.
Begin by creating and activating a new virtual environment, installing Django, and bootstrapping a new Django project.
If you need help, refer to the Quick install guide from the Django docs.
Note: The source code for this project can be found on the appsignal-django-error-tracking GitHub repo.
Install AppSignal for Django
To add AppSignal to a Django-based project, follow the AppSignal documentation:
To ensure everything works, start the development server. Your Django project will automatically send a demo error to AppSignal. In addition, you should receive an email notification.
From now on, all your app errors will be reported to AppSignal.
App Logic
Moving along, let's implement the movie review app logic.
You might notice that some code snippets contain faulty code. The defective code is placed there on purpose to later demonstrate how AppSignal error tracking works.
First, create a dedicated app for movies and reviews:
(venv)$ python manage.py startapp movies
Add the newly-created app to INSTALLED_APPS
in settings.py:
# core/settings.py
INSTALLED_APPS = [
# ...
'movies.apps.MoviesConfig',
]
Define the app's database models in models.py:
# movies/models.py
from django.db import models
class Movie(models.Model):
title = models.CharField(max_length=128)
description = models.TextField(max_length=512)
release_year = models.IntegerField()
def get_average_rating(self):
reviews = MovieReview.objects.filter(movie=self)
return sum(review.rating for review in reviews) / len(reviews)
def serialize_to_json(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'release_year': self.release_year,
}
def __str__(self):
return f'{self.title} ({self.release_year})'
class MovieReview(models.Model):
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
rating = models.IntegerField()
def save(self, force_insert=False, force_update=False, using=None, update=None):
if self.rating < 1 or self.rating > 5:
raise ValueError('Rating must be between 1 and 5.')
super().save(force_insert, force_update, using, update)
def serialize_to_json(self):
return {
'id': self.id,
'movie': self.movie.serialize_to_json(),
'rating': self.rating,
}
def __str__(self):
return f'{self.movie.title} - {self.rating}'
What's Happening Here?
- We define two models,
Movie
andMovieReview
. AMovie
can have multipleMovieReview
s. Both models include aserialize_to_json()
method for serializing the object to JSON. - The
Movie
model includesget_average_rating()
for calculating a movie's average rating. -
MovieReview
'srating
is locked in a[1, 5]
interval via the modifiedsave()
method.
Now, make migrations and migrate the database:
(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
Define the Views in Python
Next, define the views in movies/views.py:
# movies/views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from movies.models import Movie, MovieReview
def index_view(request):
queryset = Movie.objects.all()
data = [movie.serialize_to_json() for movie in queryset]
return JsonResponse(data, safe=False)
def statistics_view(request):
queryset = Movie.objects.all()
data = []
for movie in queryset:
data.append({
'id': movie.id,
'title': movie.title,
'average_rating': movie.get_average_rating(),
})
return JsonResponse(data, safe=False)
@csrf_exempt
@require_http_methods(["POST"])
def review_view(request):
movie_id = request.POST.get('movie')
rating = request.POST.get('rating')
if (movie_id is None or rating is None) or \
(not movie_id.isdigit() or not rating.isdigit()):
return JsonResponse({
'detail': 'Please provide a `movie` (int) and `rating` (int).',
}, status=400)
movie_id = int(movie_id)
rating = int(rating)
try:
movie = Movie.objects.get(id=movie_id)
MovieReview.objects.create(
movie=movie,
rating=rating,
)
return JsonResponse({
'detail': 'A review has been successfully posted',
})
except Movie.DoesNotExist:
return JsonResponse({
'detail': 'Movie does not exist.',
}, status=400)
What's Happening Here?
- We define three views:
index_view()
,statistics_view()
, andreview_view()
. - The
index_view()
fetches all the movies, serializes them, and returns them. - The
statistics_view()
calculates and returns the average rating of every movie. - The
review_view()
allows users to create a movie review by providing the movie ID and a rating.
Register the URLs
The last thing we must do is take care of the URLs.
Create a urls.py file within the movies
app with the following content:
# movies/urls.py
from django.urls import path
from movies import views
urlpatterns = [
path('statistics/', views.statistics_view, name='movies-statistics'),
path('review/', views.review_view, name='movies-review'),
path('', views.index_view, name='movies-index'),
]
Then register the app URLs globally:
# core/urls.py
from django.contrib import admin
from django.urls import path, include # new import
urlpatterns = [
path('movies/', include('movies.urls')), # new
path('admin/', admin.site.urls),
]
Using Fixtures
To get some test data to work with, I've prepared two fixtures.
First, download the fixtures, create a fixtures folder in the project root, and place them in there:
django-error-tracking/
+-- fixtures/
+-- Movie.json
+-- MovieReview.json
After that, run the following two commands to load them:
(venv)$ python manage.py loaddata fixtures/Movie.json --app app.Movie
(venv)$ python manage.py loaddata fixtures/MovieReview.json --app app.MovieReview
We now have a functional API with some sample data to work with. In the next section, we'll test it.
Test Your Django App's Errors with AppSignal
During the development of our web app, we left intentional bugs in the code. We will now deliberately trigger these bugs to see what happens when an error occurs.
Before proceeding, ensure your Django development server is running:
(venv)$ python manage.py runserver
Your API should be accessible at http://localhost:8000/movies.
AppSignal should, of course, be employed when your application is in production rather than during development, as shown in this article.
Error 1: ZeroDivisionError
Start by visiting http://localhost:8000/movies/statistics in your favorite browser. This endpoint is supposed to calculate and return average movie ratings, but it results in a ZeroDivisonError
.
Let's use AppSignal to figure out what went wrong.
First, navigate to your AppSignal dashboard and select your application. On the sidebar, you'll see several categories. Select "Errors > Issue list":
You'll see that a ZeroDivisionError
has been reported. Click on it to inspect it.
The error detail page displays the error's summary, trends, state, severity, etc. But we are interested in the samples. Select the "Samples" menu item in the navigation bar to access them.
A sample refers to a recorded instance of a specific error. Select the first sample.
The sample's detail page shows the error message, what device triggered the error, the backtrace, and more. We can look at the backtrace to determine what line of code caused the error.
In the backtrace, we can see that the error occurred in movies/models.py on line 16
. Looking at the code, the error is obvious: if a movie has no reviews, this results in a division by zero.
To fix this error, all you have to do is slightly modify the Movie
's get_average_rating()
method:
# movies/models.py
class Movie(models.Model):
# ...
def get_average_rating(self):
reviews = MovieReview.objects.filter(movie=self)
# new condition
if len(reviews) == 0:
return 0
return sum(review.rating for review in reviews) / len(reviews)
# ...
Reload the development server, test the functionality again, and set the error's state to "Closed" if everything works as expected.
Error 2: ValueError
We can trigger another error by submitting a review with a rating outside the [1, 5]
interval. To try it out, open your terminal and run the following command:
$ curl --location --request POST 'http://localhost:8000/movies/review/' \
--form 'movie=2' \
--form 'rating=6'
As expected, a ValueError
is reported to AppSignal. To track the error, follow the same procedure as in the previous section.
The error can be easily fixed by adding a check to review_view()
in views.py:
# movies/views.py
from django.core.validators import MinValueValidator, MaxValueValidator
# ...
@csrf_exempt
@require_http_methods(["POST"])
def review_view(request):
# ...
movie_id = int(movie_id)
rating = int(rating)
if rating < 1 or rating > 5:
return JsonResponse({
'detail': 'Rating must be between 1 and 5.',
}, status=400)
try:
movie = Movie.objects.get(id=movie_id)
MovieReview.objects.create(
movie=movie,
rating=rating,
)
# ...
# ...
After that, test the functionality again and tag the error as "Resolved" if everything works as expected.
Wrapping Up
In this article, you've learned how to use AppSignal to track errors in a Django application.
To get the most out of AppSignal for Python, I suggest you review the following two resources:
Happy coding!
P.S. If you'd like to read Python posts as soon as they get off the press, subscribe to our Python Wizardry newsletter and never miss a single post!
Posted on March 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.