Skip to content

v1.19.0

Compare
Choose a tag to compare
@release-drafter release-drafter released this 11 Aug 06:28
· 2915 commits to develop since this release
f627b02

Summary

This release highlights 1/ a brand new Feature Flags utility, 2/ auto-disable Tracer in non-Lambda environments to ease unit testing, 3/ API Gateway event handler now supports a custom JSON serializer, and a number of documentation improvements & bugfixes.

We hope you enjoy this new utility as much as we did working on it!!

New Feature Flags utility in Beta

Special thanks to: @risenberg-cyberark, @michaelbrewer, @pcolazurdo and @am29d

You might have heard of feature flags when:

  • Looking to conditionally enable a feature in your application for your customers
  • A/B testing a new feature for a subset of your customers
  • Working with trunk-based development where a feature might not be available right now
  • Working on short-lived features that will only be enabled for select customers

This new utility makes this entire process so much easier by fetching feature flags configuration from AWS AppConfig, and evaluating contextual values against rules you defined to decide whether a feature should be enabled.

Let's dive into the code to better understand what this all means.

Evaluating whether a customer should have access to premium features

from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore

app_config = AppConfigStore(
    environment="dev",
    application="product-catalogue",
    name="features"
)

feature_flags = FeatureFlags(store=app_config)

def lambda_handler(event, context):
    # Get customer's tier from incoming request
    ctx = { "tier": event.get("tier", "standard") }

    has_premium_features: bool = feature_flags.evaluate(name="premium_features",
                                                        context=ctx, default=False)
    if has_premium_features:
        # enable premium features
        ...

Sample feature flag configuration in AWS AppConfig

{
  "premium_features": {
    "default": false,
    "rules": {
      "customer tier equals premium": {
        "when_match": true,
        "conditions": [
          {
            "action": "EQUALS",
            "key": "tier",
            "value": "premium"
          }
        ]
      }
    }
  },
  "ten_percent_off_campaign": {
    "default": false
  }
}

Notice we have premium_features flag that will conditionally be available for premium customers, and a static feature flag named ten_percent_off_campaign that is disabled by default.

Sample invocation event for this function

{
    "username": "lessa",
    "tier": "premium",
    "basked_id": "random_id"
}

There's a LOT going on here. Allow me to break it down:

  1. We're defining a feature flag configuration that is stored in AWS AppConfig
  2. We initialize an AppConfigStore using AWS AppConfig values created via Infrastructure as code (available on docs)
  3. We initialize FeatureFlags and use our previously instantiated AppConfigStore
  4. We call evaluate method and pass the name of the premium feature, along with our contextual information our rules should run against, and a sentinel value to be used in case service errors happen
  5. Feature flags go through the rules defined in premium_features and evaluate whether tier key has the value premium
  6. FeatureFlags returns True which is then stored as has_premium_features variable

But what if you have multiple feature flags and only want all enabled features?

We've got you covered! You can use get_enabled_features to make a single call to AWS AppConfig and return a list of all enabled features at once.

from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags, AppConfigStore

app = ApiGatewayResolver()

app_config = AppConfigStore(
    environment="dev",
    application="product-catalogue",
    name="features"
)

feature_flags = FeatureFlags(store=app_config)


@app.get("/products")
def list_products():
    ctx = {
        **app.current_event.headers,
        **app.current_event.json_body
    }

    # all_features is evaluated to ["geo_customer_campaign", "ten_percent_off_campaign"]
    all_features: list[str] = feature_flags.get_enabled_features(context=ctx)

    if "geo_customer_campaign" in all_features:
        # apply discounts based on geo
        ...

    if "ten_percent_off_campaign" in all_features:
        # apply additional 10% for all customers
        ...

def lambda_handler(event, context):
    return app.resolve(event, context)

But hang on, why Beta?

We want to hear from you on the UX and evaluate how we can make it easier for you to bring your own feature flag store such as Redis, HashiCorp Consul, etc.

When would you use feature flags vs env variables vs Parameters utility?

Environment variables. For when you need simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment.

Parameters utility. For when you need access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB.

Feature flags utility. For when you need static or feature flags that will be enable conditionally based on the input and on a set of rules per feature whether that applies for all customers or on a per customer basis.

In both Parameters and Feature Flags utility you can change their config without having to change your application code.

Changes

Changes

🌟New features and non-breaking changes

  • feat(tracer): auto-disable tracer when for non-Lambda envs (#598) by @michaelbrewer
  • feat(feature-flags): Add not_in action and rename contains to in (#589) by @risenberg-cyberark
  • refactor(feature-flags): add debug for all features evaluation" (#590) by @heitorlessa
  • refactor(feature-flags): optimize UX and maintenance (#563) by @heitorlessa
  • feat(api-gateway): add support for custom serializer (#568) by @michaelbrewer
  • feat(params): expose high level max_age, raise_on_transform_error (#567) by @michaelbrewer
  • feat(data-classes): decode json_body if based64 encoded (#560) by @michaelbrewer

📜 Documentation updates

🐛 Bug and hot fixes

  • fix(feature-flags): bug handling multiple conditions (#599) by @risenberg-cyberark
  • fix(parser): apigw wss validation check_message_id; housekeeping (#553) by @michaelbrewer

🔧 Maintenance

This release was made possible by the following contributors:

@am29d, @dependabot, @dependabot[bot], @dreamorosi, @heitorlessa, @michaelbrewer, @risenberg-cyberark and @pcolazurdo