Skip to content

Commit

Permalink
add support for launch darkly and clean up some stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
z3z1ma committed Aug 1, 2022
1 parent 38a285d commit 6f769eb
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
__pycache__/
dist/
83 changes: 57 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dbt-feature-flags

## Basics
## Why Feature Flags?

> At a foundational level, feature flags enable code to be committed and deployed to production in a dormant state and then activated later. This gives teams more control over the user experience of the end product. Development teams can choose when and to which users new code is delivered. - Atlassian (Ian Buchannan)
Expand All @@ -11,30 +11,59 @@ This ELT pattern heavily encourages experimentation. dbt-feature-flags allow dbt

## Usage

This integration uses Harness Feature Flags. Sign up [here](https://harness.io/products/feature-flags). It's free to use and provides the interface for controlling your feature flags.
This integration uses Harness Feature Flags by default. Sign up [here](https://harness.io/products/feature-flags). It's free to use and provides the interface for controlling your feature flags.

Interface
![flow](https://files.helpdocs.io/kw8ldg1itf/articles/1j7pdkqh7j/1657792368788/screenshot-2022-07-14-at-10-52-03.png)

Alternatively we also support [LaunchDarkly](https://launchdarkly.com/) and the package is architected in such a way that adding a new client is fairly straightforward.

### Set Up

Supported clients

| clients | supported |
|--------------|-----------|
| harness ||
| launchdarkly ||
| unleashed | ⛔️ |

The below options are applicable to all clients unless specifically noted otherwise.

Required env vars:

`DBT_FF_API_KEY` - your feature flags key. Instructions [here](https://docs.harness.io/article/1j7pdkqh7j-create-a-feature-flag#step_3_create_an_sdk_key) to set it up
`FF_PROVIDER` - Must be one of above supported providers exactly as shown. Defaults to harness if unset out of convenience. So to override: FF_PROVIDER=launchdarkly

`DBT_FF_API_KEY` - your feature flags key. Instructions [here](https://docs.harness.io/article/1j7pdkqh7j-create-a-feature-flag#step_3_create_an_sdk_key) to set up a harness key. Because of the server-side use case with no client SDKs in play, the Harness free tier can sustain **any size** dbt deployment.

Optional:

`DBT_TARGET` - this lets you serve different flag evaluations to different targets. This variable should be set by the user/server where dbt is running and mostly intuitively correlates to dbt targets but could technically be anything you want to differentiate and serve differently. When unset, `default` is the default target value and is also reasonable if differentiating is unimportant

`DBT_FF_DISABLE` - disable the patch, note that feature_flag expressions will cause your dbt models not to compile until removed or replaced. If you have the package as a dependency and aren't using it, you can save a second of initialization

`DBT_FF_DELAY` - delay before evaluating feature flags, you shouldn't need this but feature flags have a cache that is seeded asynchronously on initialization so a small delay is required to evaluate properly. Our default delay is 1s
`DBT_FF_DELAY` - delay before evaluating feature flags, you shouldn't need this but feature flags have a cache that is seeded asynchronously on initialization so a small delay is required to evaluate properly. Our default delay is 1s (HARNESS CLIENT ONLY)

### Jinja Functions

These are available *anywhere* dbt jinja is evaluated. That includes profiles.yml, dbt_project.yml, models, macros, etc.

`feature_flag(flag: str)`: Looks for boolean variation flag. By default returns False. Most flags are boolean. Will throw RuntimeError if different return type is detected.

`feature_flag_str(flag: str)`: Looks for string variation flag. By default returns "". Will throw RuntimeError if different return type is detected.

`feature_flag_num(flag: str)`: Looks for number variation flag. By default returns 0. Will throw RuntimeError if different return type is detected.

`feature_flag_json(flag: str)`: Looks for json variation flag. By default returns an empty dict {}. Will throw RuntimeError if different return type is detected.

## Examples

A contrived example:

```sql
-- Use a feature_flag call as a bool value
{{ config(enabled=feature_flag("custom_date_model")) }}

select
*
{%- if feature_flag("new_relative_date_columns") %},
Expand All @@ -55,42 +84,44 @@ from
{{ ref('dim_dates__base') }}
```

BQ ML Model example (this could be ran in a run-operation, feature flags are valid anywhere dbt evaluates jinja)
BQ ML model example (this could be ran in a `run-operation`, feature flags are valid anywhere dbt evaluates jinja)

```sql
CREATE OR REPLACE MODEL `bqml_tutorial.penguins_model`
OPTIONS
(model_type='linear_reg',
input_label_cols=['body_mass_g']) AS
SELECT
create or replace model `bqml_tutorial.penguins_model`
options (
model_type='linear_reg',
input_label_cols=['body_mass_g'] ) as
select
*
FROM
`bigquery-public-data.ml_datasets.penguins`
WHERE
from
{{ source('ml_datasets', 'penguins') }}
where
{% if feature_flag("penguins_model_min_weight_filter") %}
body_mass_g > 100
{% else %}
body_mass_g IS NOT NULL
body_mass_g is not null
{% endif %}
```

Another BQ ML example

```sql
SELECT
select
*
FROM
ML.EVALUATE(
from
ml.evaluate(
{% if feature_flag("use_v2_ml_model") %}
MODEL `bqml_tutorial.penguins_model_v2`,
model `bqml_tutorial.penguins_model_v2`,
{% else %}
MODEL `bqml_tutorial.penguins_model`,
{% endif %}
(
SELECT
model `bqml_tutorial.penguins_model`,
{% endif %} (
select
*
FROM
`bigquery-public-data.ml_datasets.penguins`
WHERE
body_mass_g IS NOT NULL))
from
{{ source('ml_datasets', 'penguins') }}
where
body_mass_g is not null
))
```

A dbt yaml example
Expand Down
5 changes: 3 additions & 2 deletions dbt_feature_flags/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import abc
from typing import Union


class BaseFeatureFlagsClient(abc.ABC):
Expand All @@ -19,13 +20,13 @@ def string_variation(self, flag: str) -> str:
)

@abc.abstractmethod
def number_variation(self, flag: str) -> float | int:
def number_variation(self, flag: str) -> Union[float, int]:
raise NotImplementedError(
"Number feature flags are not implemented for this driver"
)

@abc.abstractmethod
def json_variation(self, flag: str) -> dict | list:
def json_variation(self, flag: str) -> Union[dict, list]:
raise NotImplementedError(
"JSON feature flags are not implemented for this driver"
)
12 changes: 10 additions & 2 deletions dbt_feature_flags/harness.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import Union

from dbt_feature_flags.base import BaseFeatureFlagsClient


class HarnessFeatureFlagsClient(BaseFeatureFlagsClient):
def __init__(self):
# Lazy imports
import atexit
import logging
import os
import time
Expand Down Expand Up @@ -31,14 +34,19 @@ def __init__(self):
self.client = CfClient(FF_KEY)
time.sleep(float(os.getenv("DBT_FF_DELAY", 1.0)))

def exit_handler(c: CfClient):
c.close()

atexit.register(exit_handler, self.client)

def bool_variation(self, flag: str) -> bool:
return self.client.bool_variation(flag, target=self.target, default=False)

def string_variation(self, flag: str) -> str:
return self.client.string_variation(flag, target=self.target, default="")

def number_variation(self, flag: str) -> float | int:
def number_variation(self, flag: str) -> Union[float, int]:
return self.client.number_variation(flag, target=self.target, default=0)

def json_variation(self, flag: str) -> dict | list:
def json_variation(self, flag: str) -> Union[dict, list]:
return self.client.json_variation(flag, target=self.target, default={})
72 changes: 72 additions & 0 deletions dbt_feature_flags/launchdarkly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Union

from dbt_feature_flags.base import BaseFeatureFlagsClient


class LaunchDarklyFeatureFlagsClient(BaseFeatureFlagsClient):
def __init__(self):
# Lazy imports
import atexit
import os

import ldclient
from ldclient.config import Config

# Set up target
self.target = {
"key": "dbt-" + os.getenv("DBT_TARGET", "default"),
"name": os.getenv("DBT_TARGET", "default").title(),
}

# Get key
FF_KEY = os.getenv("DBT_FF_API_KEY")
if FF_KEY is None:
raise RuntimeError(
"dbt-feature-flags injected in environment, this patch requires the env var DBT_FF_API_KEY"
)

# Init client
ldclient.set_config(Config(FF_KEY))
self.client = ldclient.get()
if not self.client.is_initialized():
raise RuntimeError(
"LaunchDarkly SDK failed to initialize, ensure (DBT_FF_API_KEY=%s) is correct.",
FF_KEY,
)

def exit_handler(c: ldclient.LDClient):
c.close()

atexit.register(exit_handler, self.client)

def bool_variation(self, flag: str) -> bool:
v = self.client.variation(flag, user=self.target, default=False)
if not isinstance(v, bool):
raise ValueError(
"Non boolean return type found for feature_flag call, use appropriate feature_flag_* call"
)
return v

def string_variation(self, flag: str) -> str:
v = self.client.variation(flag, user=self.target, default="")
if not isinstance(v, str):
raise ValueError(
"Non string return type found for feature_flag_str call, use appropriate feature_flag_* call"
)
return v

def number_variation(self, flag: str) -> Union[float, int]:
v = self.client.variation(flag, user=self.target, default=0)
if not isinstance(v, (float, int)):
raise ValueError(
"Non number return type found for feature_flag_num call, use appropriate feature_flag_* call"
)
return v

def json_variation(self, flag: str) -> Union[dict, list]:
v = self.client.variation(flag, user=self.target, default={})
if not isinstance(v, (dict, list)):
raise ValueError(
"Non JSON return type found for feature_flag_json call, use appropriate feature_flag_* call"
)
return v
4 changes: 3 additions & 1 deletion dbt_feature_flags/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
import os

from dbt_feature_flags import base, harness
from dbt_feature_flags import base, harness, launchdarkly


def _get_client() -> base.BaseFeatureFlagsClient:
Expand All @@ -13,6 +13,8 @@ def _get_client() -> base.BaseFeatureFlagsClient:
ff_client = None
if ff_provider == "harness":
ff_client = harness.HarnessFeatureFlagsClient()
elif ff_provider == "launchdarkly":
ff_client = launchdarkly.LaunchDarklyFeatureFlagsClient()
if not isinstance(ff_client, base.BaseFeatureFlagsClient):
raise RuntimeError(
"Invalid feature flag client specified by (FF_PROVIDER=%s)",
Expand Down
Loading

0 comments on commit 6f769eb

Please sign in to comment.