Motivation
Any non-trivial project I’ve ever worked on sooner or later had a need for feature flags (also called feature toggles). They can be used for awesome stuff but they can also be used for terrible things. It’s really up to you and your team.
The goal
At the end of this tutorial you will know
- how to implement feature flags in django
- how to use a third-party feature-flag-provider to manage your feature-flags
- have an idea how this can be used to roll-out new features to a subset of your users (e.g. only staff-members)
Getting the project source-code
The example-project can be found on github. Here are the steps I used to create the initial commit. I assume you have pipenv installed (and, of course, Python 3):
Here is what you need to get the project files:
# Follow these instructions to install the project in ./django_feature_flags_example
$ git clone https://github.com/steuke/django_feature_flags_example.git
Cloning into 'django_feature_flags_example'...
remote: Enumerating objects: 41, done.
remote: Counting objects: 100% (41/41), done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 41 (delta 9), reused 41 (delta 9), pack-reused 0
Unpacking objects: 100% (41/41), done.
Let’s create a virtual environment, activate it, and install the dependencies:
(Are you using pipenv
? Then you can simply run pipenv install
in the git-folder that contains the Pipfile
and skip this part.)
$ cd django_feature_flags_example
$ python3 -m venv ./venv
$ source ./venv/bin/activate
$ pip install -r requirements.txt
Verify that it all worked:
$ feature_flags_project/manage.py
Traceback (most recent call last):
File "feature_flags_project/manage.py", line 21, in <module>
...
ValueError: You must supply a valid Optimizely SDK-key. Did you remember to set settings.OPTIMIZELY_SDK_KEY?
If you see the above error, then you are all set up to follow this tutorial. :)
Important: The example-project is NOT PRODUCTION-READY. It is meant as a convenient way to follow this tutorial. More information can be found in the Django deployment check-list.
Project Layout
feature_flags_project/
├── feature_flags_project # django project folder
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py # <--- you need to add the Optimizely key here (later on)
│ ├── urls.py
│ └── wsgi.py
├── feature_flags # feature flag module (you could copy this to your own project)
│ ├── __init__.py
│ ├── features.py
│ ├── optimizely_provider.py # sample implementation of a feature-flag provider
│ └── providers.py
├── manage.py
└── my_app # sample django app we use to demonstrate feature flags
├── __init__.py
├── apps.py
├── my_features.py # contains your feature flag definitions
├── templates
│ └── index.html # shows how to use feature flags to show/hide html elements
├── templatetags
│ ├── __init__.py
│ └── my_feature_flags.py # custom template tag for your feature flags
└── views.py
Create an Optimizely Rollouts account
Sign-up for a free Optimizely Rollouts account.
Why Optimizely? Because it’s free, and I’ve used it before. (There are others: LaunchDarkly, ConfigCat, Cloudbees Rollout etc.) The benefits of using a third-party service is that you will get a robust implementation to manage your feature-flags, including a nice GUI. (If you want to use a different feature-flag-provider (or create your own), you need to subclass feature_flags.providers.FeatureFlagProvider
and provide an instance of it to settings.FEATURE_FLAG_PROVIDER
.)
Find the OPTIMIZELY_SDK_KEY
Log into Optimizely and go to “Settings”. You should find two default environments that Optimizely has created for you. We will be using the
development
-environment.Copy the SDK-Key for the
development
-environment to thesettings.py
.
Replace this:
OPTIMIZELY_SDK_KEY = None
with this (using your own Optimizely SDK-Key):
OPTIMIZELY_SDK_KEY = "w348t7cznw8t7UZBUB"
Re-create the sample feature in your Optimizely account
The sample django-project comes with a feature named enable_awesome_text_feature
. In order to actually be able to turn the feature on and off via Optimizely, you need to create this feature in your Optimizely account (using their UI). This is what it should look like. Make sure to use the exact same name:
(Note: If you use a different name, the sample code will not work. I’ll show you how to create your own feature below.)
Take it for a spin
Open a shell and cd
into the project django-project directory (the one containing the manage.py
file) and run the local development server:
$ ./manage.py runserver
...
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Now open http://127.0.0.1:8000/ in your favorite browser, and you should see this text:
Did it work? We cannot be sure until we change the feature from disabled
to enabled
in Optimizely. Let’s do that now.
Switching the feature ON and checking if it worked
- Go to the Optimizely dashboard and edit the feature settings by clicking the feature’s name.
- Enable the feature for the correct stage (or all stages, if you are not sure).
- Be sure to click “Save” at the end!
Verify that it worked
For the change to take effect, you have to wait 20 seconds. (This is configured using the settings.OPTIMIZELY_UPDATE_INTERVAL_SECONDS
. In a real-life application you would probably increase this time to something like 5 or 10 minutes.) Then reload the page http://127.0.0.1:8000/. It should now show this awesome text (is promised):
In addition, the console log will show the following line:
INFO Feature 'enable_awesome_text_feature' is enabled=True
Creating your own feature
Let’s say you want to create your own, crazy feature. Here are the steps todo just that:
- create the feature in Optimizely (we’ll name it
enable_crazy_feature
) - add the feature to
my_app/my_features.py
- (optional) create a custom template-tag, so that you can use the feature-flag in django-templates
We will go through this step-by-step.
Adding your new feature to Optimizely
This is the same as before, only with a different name. Click “Create New Feature” and use the name you chose. We will use enable_crazy_feature
as the name.
Adding your feature to my_app/my_features.py
Add the following line at the end of the file:
ENABLE_CRAZY_FEATURE = Feature(name="enable_crazy_feature")
The above code will register the new feature-flag with our feature-flags-module.
To use your new feature-flag you simply import it and call it’s is_enabled()
method:
from my_app.my_features import ENABLE_CRAZY_FEATURE
if ENABLE_CRAZY_FEATURE.is_enabled():
print("We are in crazy mode!")
else:
print("Nothing crazy is going on.")
Adding a custom template-tag for your feature (optional)
If you want to use the feature-flag in a template, you can create a simple custom tag. Add the following lines to my_app/templatetags/my_feature_flags.py
:
from my_app.my_features import ENABLE_CRAZY_FEATURE
@register.simple_tag
def enable_crazy_feature() -> bool:
return ENABLE_CRAZY_FEATURE.is_enabled()
Using the custom template-tag in your templates to show some text (optional)
Using this new template-tag is a little awkward, because you have to define a variable inside your template to use in your if-statement. Here is how it’s done:
{% enable_crazy_feature as crazy_feature_enabled %}
{% if crazy_feature_enabled %}
<h1>And this is just crazy! 🥳</h1>
{% else %}
<p>Not crazy at all.</p>
{% endif %}
Using Optimizely-Audiences to roll-out features to a subset of your users
While this is beyond the scope of this introduction, I wanted to give you an idea how feature-flags can be even more customized. The feature-flags you have seen until now are global feature flags, i.e. they are either enabled or disabled for all your users.
What if you want to enable a certain feature only for a subset of users, maybe only the staff-members (based on Django’s built-in is_staff
user-property?
This can be done by using Optimizely’s audiences and attributes:
- within Optimizely, create an attribute (e.g. “is_staff”)
- within Optimizely, create an audience “Staff-Members” with a condition “is_staff equals true”
- within Optimizely, edit your feature and set its audience to “Staff-Members” (instead of “Everyone”)
- in your code, set the attribute “is_staff” to “true” for all staff-members.
The latter has been already implemented in the sample project: Have a look at the _attributes_from_request()
-method (located in feature_flags/providers.py
).
Things you’ll want to add
There a many things that are missing from the sample project. In a production project you would at least need the following:
- automated tests
- a way to handle different environments/stages (development, testing, staging, production)
- a mock-provider for testing
In addition, you’ll need to think about how you will handle database-migrations and roll-backs, but that’s a whole different topic for another day.
If you liked this post, please, do share it:
Thanks, for reading (and sharing)! 🥳