Track Errors in Your Python Flask Application with AppSignal
Tags: python, flask
Published: 2024-06-12 10:13:07

In this article, we'll look at how to track errors in a Flask application using AppSignal.

We'll first bootstrap a Flask project, and install and configure AppSignal. Then, we'll introduce some faulty code and demonstrate how to track and resolve errors using AppSignal's Errors dashboard.

Let's get started!

## Prerequisites

Before diving into the article, ensure you have:

- [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) (you can start a free 30-day trial)
- Fundamental Flask knowledge

## Project Setup

To demonstrate how AppSignal error tracking works, we'll create a simple TODO app. The app will provide a RESTful API that supports CRUD operations. Initially, it will contain some faulty code, which we'll address later.

> I recommend you first follow along with this exact project since the article is tailored to it. After the article, you'll, of course, be able to integrate AppSignal into your own Flask projects.

Start by bootstrapping a Flask project:

1. Create and activate a virtual environment
2. Use pip to install the latest version of Flask
3. Start the development server

> If you get stuck, refer to the [Flask Installation guide](https://flask.palletsprojects.com/en/3.0.x/installation/).

**Note:** The source code for this project can be found in the [appsignal-flask-error-tracking](https://github.com/duplxey/appsignal-flask-error-tracking) GitHub repo.

## Install AppSignal for Flask

To add AppSignal to your Flask project, follow the AppSignal documentation:

1. [AppSignal Python Installation](https://docs.appsignal.com/python/installation)
2. [AppSignal Flask Instrumentation](https://docs.appsignal.com/python/instrumentations/flask.html)

Ensure everything works by starting the development server:

```sh
(venv)$ flask run
```

Your app should automatically send a demo error to AppSignal. From now on, all your app errors will be forwarded to AppSignal.

> If you get an error saying `Failed to find Flask application`, you most likely imported Flask before starting the AppSignal client. As mentioned in the [docs](https://docs.appsignal.com/python/instrumentations/flask.html#setup), AppSignal has to be imported and started at the top of _app.py_.

## Flask for Python App Logic

Moving along, let's implement the web app logic.

### Flask-SQLAlchemy for the Database

We'll use the [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/) package to manage the database. This package provides [SQLAlchemy](https://www.sqlalchemy.org/) support to Flask projects. That includes the Python SQL toolkit and the ORM.

First, install it via pip:

```sh
(venv)$ pip install Flask-SQLAlchemy
```

Then initialize the database and Flask:

```py
# app.py

db = SQLAlchemy()
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///default.db"
db.init_app(app)
```

Don't forget about the import:

```python
from flask_sqlalchemy import SQLAlchemy
```

Next, create the `Task` database model:

```py
# app.py

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    description = db.Column(db.Text(512), nullable=True)

    created_at = db.Column(db.DateTime, default=db.func.now())
    updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

    is_done = db.Column(db.Boolean, default=False)

    def as_dict(self):
        return {c.name: getattr(self, c.name) for c in self.__table__.columns}

    def __repr__(self):
        return f"<Task {self.id}>"
```

Each `Task` will have a `name`, an optional `description`, an `is_done` field, and some administrative data. To serialize the `Task`, we'll use its `as_dict()` method.

Since Flask-SQLAlchemy doesn't automatically create the database and its structure, we must do it ourselves. To handle that, we'll create a simple Python script.

Create an _init_db.py_ file in the project root with the following content:

```py
# init_db.py

from app import Task
from app import db, app

with app.app_context():
    db.create_all()

    if Task.query.count() == 0:
        tasks = [
            Task(
                name="Deploy App",
                description="Deploy the Flask app to the cloud.",
                is_done=False,
            ),
            Task(
                name="Optimize DB",
                description="Optimize the database access layer.",
                is_done=False,
            ),
            Task(
                name="Install AppSignal",
                description="Install AppSignal to track errors.",
                is_done=False,
            ),
        ]

        for task in tasks:
            db.session.add(task)

        db.session.commit()
```

#### What's Happening Here?

This script performs the following:

1. Fetches Flask's app instance.
2. Creates the database and its structure via `db.create_all()`.
3. Populates the database with three sample tasks.
4. Commits all the changes to the database via `db.session.commit()`.

### Defining Views

Define the views in _app.py_ like so:

```python
# app.py

@app.route("/")
def list_view():
    tasks = Task.query.all()
    return jsonify([task.as_dict() for task in tasks])


@app.route("/<int:task_id>", methods=["GET"])
def detail_view(task_id):
    task = db.get_or_404(Task, task_id)
    return jsonify(task.as_dict())


@app.route("/create", methods=["POST"])
def create_view():
    name = request.form.get("name", type=str)
    description = request.form.get("description", type=str)

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201


@app.route("/toggle-done/<int:task_id>", methods=["PATCH"])
def toggle_done_view(task_id):
    task = db.get_or_404(Task, task_id)

    task.is_done = not task.is_done
    db.session.commit()

    return jsonify(task.as_dict())


@app.route("/delete/<int:task_id>", methods=["DELETE"])
def delete_view(task_id):
    task = db.get_or_404(Task, task_id)

    db.session.delete(task)
    db.session.commit()

    return jsonify({}), 204


@app.route("/statistics", methods=["GET"])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()
    done_percentage = done_tasks_count / (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        "done_tasks_count": done_tasks_count,
        "undone_tasks_count": undone_tasks_count,
        "done_percentage": done_percentage,
    })
```

Don't forget about the import:

```python
from flask import jsonify, request
```

#### What's Happening Here?

1. We define six API endpoints.
2. The `list_view()` fetches all the tasks, serializes and returns them.
3. The `detail_view()` fetches a specific task, serializes and returns it.
4. The `create_view()` creates a new task from the provided data.
5. `toggle_done_view()` toggles the task's `is_done` property.
6. The `delete_view()` deletes a specific task.
7. The `statistics_view()` calculates general app statistics.

Great, we've successfully created a simple TODO web app!

## Test Your Python Flask App's Errors with AppSignal

During the development of our web app, we intentionally left in some faulty code. We'll now trigger these bugs to see what happens when an error occurs.

Before proceeding, ensure your Flask development server is running:

```python
(venv)$ flask run --debug
```

Your API should be accessible at [http://localhost:5000/](http://localhost:5000/).

> AppSignal should, of course, be employed when your application is in production rather than during development, as shown in this article.

### Error 1: `OperationalError`

To trigger the first error, request the task list:

```sh
$ curl --location 'localhost:5000/'
```

This will return an `Internal Server Error`. Let's use AppSignal to figure out what went wrong.

Open your favorite web browser and navigate to your [AppSignal dashboard](https://appsignal.com/accounts). Select your organization and then your application. Lastly, choose "Errors > Issue list" on the sidebar:

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

You'll see that an `OperationalError` was reported. Click on it to inspect it:

![AppSignal Errors Issue Details](https://blog.appsignal.com/images/blog/2024-05/appsignal-errors-issue-details.png)

The error detail page will display the error message, backtrace, state, trends, and so on.

We can figure out what went wrong just by looking at the error message. `no such table: task` tells us that we forgot to initialize the database.

To fix that, run the previously created script:

```sh
(venv)$ python init_db.py
```

Retest the app and mark the issue as "Closed" once you've verified everything works.

![AppSignal Errors Issue Tag Closed](https://blog.appsignal.com/images/blog/2024-05/appsignal-errors-mark-as-closed.png)

### Error 2: `IntegrityError`

Let's trigger the next error by trying to create a task without a `name`:

```sh
$ curl --location 'localhost:5000/create' \
       --form 'description="Test the web application."'
```

Open the AppSignal dashboard and navigate to the `IntegrityError`'s details.

Now instead of just checking the error message, select "Samples" in the navigation:

![AppSignal Errors Samples](https://blog.appsignal.com/images/blog/2024-05/appsignal-errors-samples.png)

A sample refers to a recorded instance of a specific error. Select the first sample.

![AppSignal Errors Sample Details](https://blog.appsignal.com/images/blog/2024-05/appsignal-errors-sample-details.png)

By checking the backtrace, we can see exactly what line caused the error. As you can see, the error happened in _app.py_ on line 53 when we tried saving the task to the database.

To fix it, provide a `default` when assigning the `name` variable:

```python
# app.py

@app.route("/create", methods=["POST"])
def create_view():
    name = request.form.get("name", type=str, default="Unnamed Task")  # new
    description = request.form.get("description", type=str, default="")

    task = Task(name=name, description=description)
    db.session.add(task)
    db.session.commit()

    return jsonify(task.as_dict()), 201
```

### Error 3: `ZeroDivisionError`

Now we'll delete all tasks and then calculate the statistics by running the following commands:

```sh
$ curl --location --request DELETE 'localhost:5000/delete/1'
$ curl --location --request DELETE 'localhost:5000/delete/2'
$ curl --location --request DELETE 'localhost:5000/delete/3'
$ curl --location --request DELETE 'localhost:5000/delete/4'
$ curl --location --request DELETE 'localhost:5000/delete/5'
$ curl --location 'localhost:5000/statistics'
```

As expected, a `ZeroDivisonError` is raised. To track the error, follow the same approach as described in the previous section.

![AppSignal ZeroDivisionError Sample](https://blog.appsignal.com/images/blog/2024-05/appsignal-errors-sample-backtrace.png)

To fix it, add a zero check to the _statistics_view()_ endpoint like so:

```python
# app.py

@app.route("/statistics", methods=["GET"])
def statistics_view():
    done_tasks_count = Task.query.filter_by(is_done=True).count()
    undone_tasks_count = Task.query.filter_by(is_done=False).count()

    # new
    if done_tasks_count + undone_tasks_count == 0:
        done_percentage = 0
    else:
        done_percentage = done_tasks_count / \
                          (done_tasks_count + undone_tasks_count) * 100

    return jsonify({
        "done_tasks_count": done_tasks_count,
        "undone_tasks_count": undone_tasks_count,
        "done_percentage": done_percentage,
    })
```

Retest the endpoint and mark it as "Closed" once you've verified the issue has been resolved.

## Manual Tracking

By default, errors are only reported to AppSignal when exceptions are left unhandled. However, in some cases, you may want handled exceptions to be reported.

To accomplish this, you can utilize AppSignal's helper methods:

1. [`set_error()`](https://docs.appsignal.com/python/instrumentation/exception-handling.html#set_error)
2. [`send_error()`](https://docs.appsignal.com/python/instrumentation/exception-handling.html#send_error) and [`send_error_with_context()`](https://docs.appsignal.com/python/instrumentation/exception-handling.html#additional-metadata)

## Wrapping Up

In this article, we've covered how to monitor errors in a Flask app using AppSignal.

We explored two error reporting methods: automatic tracking and manual tracking (using helper methods). With this knowledge, you can easily incorporate AppSignal into your Flask projects.

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!](https://blog.appsignal.com/python-wizardry)**