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+ installed on your local machine
- An AppSignal-supported operating system
- An AppSignal account (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:
- Create and activate a virtual environment
- Use pip to install the latest version of Flask
- Start the development server
If you get stuck, refer to the Flask Installation guide.
Note: The source code for this project can be found in the appsignal-flask-error-tracking GitHub repo.
Install AppSignal for Flask
To add AppSignal to your Flask project, follow the AppSignal documentation:
Ensure everything works by starting the development server:
(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, 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 package to manage the database. This package provides SQLAlchemy support to Flask projects. That includes the Python SQL toolkit and the ORM.
First, install it via pip:
(venv)$ pip install Flask-SQLAlchemy
Then initialize the database and Flask:
# 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:
from flask_sqlalchemy import SQLAlchemy
Next, create the Task
database model:
# 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:
# 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:
- Fetches Flask's app instance.
- Creates the database and its structure via
db.create_all()
. - Populates the database with three sample tasks.
- Commits all the changes to the database via
db.session.commit()
.
Defining Views
Define the views in app.py like so:
# 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:
from flask import jsonify, request
What's Happening Here?
- We define six API endpoints.
- The
list_view()
fetches all the tasks, serializes and returns them. - The
detail_view()
fetches a specific task, serializes and returns it. - The
create_view()
creates a new task from the provided data. toggle_done_view()
toggles the task'sis_done
property.- The
delete_view()
deletes a specific task. - 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:
(venv)$ flask run --debug
Your API should be accessible at 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:
$ 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. Select your organization and then your application. Lastly, choose "Errors > Issue list" on the sidebar:
You'll see that an OperationalError
was reported. Click on it to inspect it:
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:
(venv)$ python init_db.py
Retest the app and mark the issue as "Closed" once you've verified everything works.
Error 2: IntegrityError
Let's trigger the next error by trying to create a task without a name
:
$ 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:
A sample refers to a recorded instance of a specific error. Select the first sample.
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:
# 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:
$ 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.
To fix it, add a zero check to the statistics_view() endpoint like so:
# 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:
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!