Track Errors in Your Python Django Application with AppSignal
Tags: python, django, errors
Published: 2024-03-13 12:12:17

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+](https://www.python.org/downloads/) installed on your local machine
- An [AppSignal-supported operating system](https://docs.appsignal.com/support/operating-systems.html)
- An [AppSignal account](https://appsignal.com/users/sign_in)
- 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](https://www.djangoproject.com/).

> 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](https://docs.djangoproject.com/en/4.2/intro/install/) from the [Django docs](https://docs.djangoproject.com/en/4.2/).

_**Note:** The source code for this project can be found on the [appsignal-django-error-tracking](https://github.com/duplxey/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](https://docs.appsignal.com/python/installation)
2. [Django instrumentation](https://docs.appsignal.com/python/instrumentations/django.html)

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 `MovieReview`s. 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](https://github.com/duplxey/appsignal-django-error-tracking/tree/master/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:

```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](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](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](https://appsignal.com/accounts) and select your application. On the sidebar, you'll see several categories. Select "Errors > Issue list":

![AppSignal Issue List](https://blog.appsignal.com/images/blog/2024-02/django-issue-list.png)

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

![AppSignal ValueError Issue](https://blog.appsignal.com/images/blog/2024-02/django-division-by-zero.png)

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](https://blog.appsignal.com/images/blog/2024-02/django-division-by-zero-sample.png)

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](https://blog.appsignal.com/images/blog/2024-02/django-tag-incident-as-closed.png)

### 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](https://blog.appsignal.com/images/blog/2024-02/django-value-error.png)

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](https://docs.appsignal.com/python/configuration.html)
2. [AppSignal's Python exception handling docs](https://docs.appsignal.com/python/instrumentation/exception-handling.html)

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](/python-wizardry)!**