python

Track Errors in Your Python Django Application with AppSignal

Nik Tomazic

Nik Tomazic on

Track Errors in Your Python Django Application with AppSignal

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:

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:

  1. AppSignal Python installation
  2. Django instrumentation

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:

sh
(venv)$ python manage.py startapp movies

Add the newly-created app to INSTALLED_APPS in settings.py:

python
# core/settings.py INSTALLED_APPS = [ # ... 'movies.apps.MoviesConfig', ]

Define the app's database models in models.py:

python
# 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?

  1. We define two models, Movie and MovieReview. A Movie can have multiple MovieReviews. Both models include a serialize_to_json() method for serializing the object to JSON.
  2. The Movie model includes get_average_rating() for calculating a movie's average rating.
  3. MovieReview's rating is locked in a [1, 5] interval via the modified save() method.

Now, make migrations and migrate the database:

sh
(venv)$ python manage.py makemigrations (venv)$ python manage.py migrate

Define the Views in Python

Next, define the views in movies/views.py:

python
# 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?

  1. We define three views: index_view(), statistics_view(), and review_view().
  2. The index_view() fetches all the movies, serializes them, and returns them.
  3. The statistics_view() calculates and returns the average rating of every movie.
  4. 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:

python
# 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:

python
# 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:

shell
django-error-tracking/ +-- fixtures/ +-- Movie.json +-- MovieReview.json

After that, run the following two commands to load them:

sh
(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:

sh
(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":

AppSignal Issue List

You'll see that a ZeroDivisionError has been reported. Click on it to inspect it.

AppSignal ValueError Issue

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.

AppSignal ValueError Issue 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:

python
# 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.

AppSignal ValueError Issue Close

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:

sh
$ 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.

AppSignal ValueError Details

The error can be easily fixed by adding a check to review_view() in views.py:

python
# 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:

  1. AppSignal's Python configuration docs
  2. AppSignal's Python exception handling docs

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!

Nik Tomazic

Nik Tomazic

Guest author Nik is a full-stack developer and technical writer who specializes in building scalable web and mobile applications. His favorite technologies include Python, TypeScript, Django, React, and Docker.

All articles by Nik Tomazic

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps