Track Errors in Your Python Django Application with AppSignal

duplxey

Nik Tomazic

Posted on March 13, 2024

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:

(venv)$ python manage.py startapp movies
Enter fullscreen mode Exit fullscreen mode

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

# core/settings.py

INSTALLED_APPS = [
    # ...
    'movies.apps.MoviesConfig',
]
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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:

(venv)$ python manage.py makemigrations
(venv)$ python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

# 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'),
]
Enter fullscreen mode Exit fullscreen mode

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),
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

# 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)

    # ...
Enter fullscreen mode Exit fullscreen mode

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:

$ curl --location --request POST 'http://localhost:8000/movies/review/' \
     --form 'movie=2' \
     --form 'rating=6'
Enter fullscreen mode Exit fullscreen mode

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:

# 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,
        )
        # ...

    # ...
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
duplxey
Nik Tomazic

Posted on March 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related